feat: parse and use Kotlin metadata for renames (#1861)(PR #1860)

* initial setup for data class and metadata parsing
* bug fix & comments
* better version using plugin system
* added tests
* ignore getters test fix
* logs & imports
* reverted accidental changes
* moved util classes to plugin, spotless apply
* move test and some other minor fixes

---------

Co-authored-by: Skylot <skylot@gmail.com>
This commit is contained in:
Krzysztof Iwaniuk 2023-05-14 22:34:40 +02:00 committed by GitHub
parent 3474f0da04
commit ccdbb1d62c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1328 additions and 327 deletions

View File

@ -154,6 +154,17 @@ Plugin options (-P<name>=<value>):
2) java-convert: Convert .class, .jar and .aar files to dex
- java-convert.mode - convert mode, values: [dx, d8, both], default: both
- java-convert.d8-desugar - use desugar in d8, values: [yes, no], default: no
3) kotlin-metadata: Use kotlin.Metadata annotation for code generation
- kotlin-metadata.class-alias - rename class alias, values: [yes, no], default: yes
- kotlin-metadata.method-args - rename function arguments, values: [yes, no], default: yes
- kotlin-metadata.fields - rename fields, values: [yes, no], default: yes
- kotlin-metadata.companion - rename companion object, values: [yes, no], default: yes
- kotlin-metadata.data-class - add data class modifier, values: [yes, no], default: yes
- kotlin-metadata.to-string - rename fields using toString, values: [yes, no], default: yes
- kotlin-metadata.getters - rename simple getters to field names, values: [yes, no], default: yes
4) rename-mappings: various mappings support
- rename-mappings.format - mapping format, values: [auto, TINY, TINY_2, ENIGMA, ENIGMA_DIR, MCP, SRG, TSRG, TSRG2, PROGUARD], default: auto
- rename-mappings.invert - invert mapping, values: [yes, no], default: no
Examples:
jadx -d out classes.dex

View File

@ -9,6 +9,8 @@ dependencies {
runtimeOnly(project(':jadx-plugins:jadx-java-input'))
runtimeOnly(project(':jadx-plugins:jadx-java-convert'))
runtimeOnly(project(':jadx-plugins:jadx-smali-input'))
runtimeOnly(project(':jadx-plugins:jadx-rename-mappings'))
runtimeOnly(project(':jadx-plugins:jadx-kotlin-metadata'))
runtimeOnly(project(':jadx-plugins:jadx-script:jadx-script-plugin'))
implementation 'com.beust:jcommander:1.82'

View File

@ -157,9 +157,6 @@ public class JadxCLIArgs {
@Parameter(names = { "--deobf-use-sourcename" }, description = "use source file name as class name alias")
protected boolean deobfuscationUseSourceNameAsAlias = false;
@Parameter(names = { "--deobf-parse-kotlin-metadata" }, description = "parse kotlin metadata to class and package names")
protected boolean deobfuscationParseKotlinMetadata = false;
@Parameter(
names = { "--deobf-res-name-source" },
description = "better name source for resources:"
@ -305,7 +302,6 @@ public class JadxCLIArgs {
args.setDeobfuscationMinLength(deobfuscationMinLength);
args.setDeobfuscationMaxLength(deobfuscationMaxLength);
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
args.setParseKotlinMetadata(deobfuscationParseKotlinMetadata);
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
args.setResourceNameSource(resourceNameSource);
args.setEscapeUnicode(escapeUnicode);
@ -443,10 +439,6 @@ public class JadxCLIArgs {
return deobfuscationUseSourceNameAsAlias;
}
public boolean isDeobfuscationParseKotlinMetadata() {
return deobfuscationParseKotlinMetadata;
}
public ResourceNameSource getResourceNameSource() {
return resourceNameSource;
}

View File

@ -18,7 +18,6 @@ dependencies {
testRuntimeOnly(project(':jadx-plugins:jadx-java-convert'))
testRuntimeOnly(project(':jadx-plugins:jadx-java-input'))
testRuntimeOnly(project(':jadx-plugins:jadx-raung-input'))
testRuntimeOnly(project(':jadx-plugins:jadx-rename-mappings'))
testImplementation 'org.eclipse.jdt:ecj:3.33.0'
testImplementation 'tools.profiler:async-profiler:2.9'

View File

@ -89,7 +89,6 @@ public class JadxArgs implements Closeable {
private boolean deobfuscationOn = false;
private boolean useSourceNameAsClassAlias = false;
private boolean parseKotlinMetadata = false;
private File generatedRenamesMappingFile = null;
private GeneratedRenamesMappingFileMode generatedRenamesMappingFileMode = GeneratedRenamesMappingFileMode.getDefault();
@ -404,14 +403,6 @@ public class JadxArgs implements Closeable {
this.useSourceNameAsClassAlias = useSourceNameAsClassAlias;
}
public boolean isParseKotlinMetadata() {
return parseKotlinMetadata;
}
public void setParseKotlinMetadata(boolean parseKotlinMetadata) {
this.parseKotlinMetadata = parseKotlinMetadata;
}
public int getDeobfuscationMinLength() {
return deobfuscationMinLength;
}
@ -640,7 +631,7 @@ public class JadxArgs implements Closeable {
+ inlineAnonymousClasses + inlineMethods + moveInnerClasses + allowInlineKotlinLambda
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength
+ resourceNameSource
+ parseKotlinMetadata + useKotlinMethodsForVarNames
+ useKotlinMethodsForVarNames
+ insertDebugLines + extractFinally
+ debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts
+ respectBytecodeAccModifiers + fsCaseSensitive + renameFlags
@ -668,7 +659,6 @@ public class JadxArgs implements Closeable {
+ ", generatedRenamesMappingFileMode=" + generatedRenamesMappingFileMode
+ ", resourceNameSource=" + resourceNameSource
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
+ ", parseKotlinMetadata=" + parseKotlinMetadata
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
+ ", insertDebugLines=" + insertDebugLines
+ ", extractFinally=" + extractFinally

View File

@ -164,6 +164,10 @@ public class AccessInfo {
return (accFlags & AccessFlags.MODULE) != 0;
}
public boolean isData() {
return (accFlags & AccessFlags.DATA) != 0;
}
public AFType getType() {
return type;
}
@ -220,6 +224,9 @@ public class AccessInfo {
code.append("strict ");
}
if (showHidden) {
if (isData()) {
code.append("/* data */ ");
}
if (isModuleInfo()) {
code.append("/* module-info */ ");
}

View File

@ -1,25 +0,0 @@
package jadx.core.dex.visitors.rename;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.kotlin.ClsAliasPair;
import jadx.core.utils.kotlin.KotlinMetadataUtils;
public class KotlinMetadataRename {
public static void process(RootNode root) {
if (root.getArgs().isParseKotlinMetadata()) {
for (ClassNode cls : root.getClasses()) {
if (cls.contains(AFlag.DONT_RENAME)) {
continue;
}
ClsAliasPair kotlinCls = KotlinMetadataUtils.getClassAlias(cls);
if (kotlinCls != null) {
cls.rename(kotlinCls.getName());
cls.getPackageNode().rename(kotlinCls.getPkg());
}
}
}
}
}

View File

@ -40,7 +40,6 @@ public class RenameVisitor extends AbstractVisitor {
}
private void process(RootNode root) {
KotlinMetadataRename.process(root);
SourceFileRename.process(root);
UserRenames.apply(root);

View File

@ -1,24 +0,0 @@
package jadx.core.utils.kotlin;
public class ClsAliasPair {
private final String pkg;
private final String name;
public ClsAliasPair(String pkg, String name) {
this.pkg = pkg;
this.name = name;
}
public String getPkg() {
return pkg;
}
public String getName() {
return name;
}
@Override
public String toString() {
return pkg + '.' + name;
}
}

View File

@ -1,115 +0,0 @@
package jadx.core.utils.kotlin;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.plugins.input.data.annotations.EncodedType;
import jadx.api.plugins.input.data.annotations.EncodedValue;
import jadx.api.plugins.input.data.annotations.IAnnotation;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.nodes.RenameReasonAttr;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.utils.Utils;
// TODO: parse data from d1 (protobuf encoded) to get original method names and other useful info
public class KotlinMetadataUtils {
private static final Logger LOG = LoggerFactory.getLogger(KotlinMetadataUtils.class);
private static final String KOTLIN_METADATA_ANNOTATION = "Lkotlin/Metadata;";
private static final String KOTLIN_METADATA_D2_PARAMETER = "d2";
/**
* Try to get class info from Kotlin Metadata annotation
*/
@Nullable
public static ClsAliasPair getClassAlias(ClassNode cls) {
IAnnotation metadataAnnotation = cls.getAnnotation(KOTLIN_METADATA_ANNOTATION);
List<EncodedValue> d2Param = getParamAsList(metadataAnnotation, KOTLIN_METADATA_D2_PARAMETER);
if (d2Param == null || d2Param.isEmpty()) {
return null;
}
EncodedValue firstValue = d2Param.get(0);
if (firstValue == null || firstValue.getType() != EncodedType.ENCODED_STRING) {
return null;
}
try {
String rawClassName = ((String) firstValue.getValue()).trim();
if (rawClassName.isEmpty()) {
return null;
}
String clsName = Utils.cleanObjectName(rawClassName);
ClsAliasPair alias = splitAndCheckClsName(cls, clsName);
if (alias != null) {
RenameReasonAttr.forNode(cls).append("from Kotlin metadata");
return alias;
}
} catch (Exception e) {
LOG.error("Failed to parse kotlin metadata", e);
}
return null;
}
// Don't use ClassInfo facility to not pollute class into cache
private static ClsAliasPair splitAndCheckClsName(ClassNode originCls, String fullClsName) {
if (!NameMapper.isValidFullIdentifier(fullClsName)) {
return null;
}
String pkg;
String name;
int dot = fullClsName.lastIndexOf('.');
if (dot == -1) {
pkg = "";
name = fullClsName;
} else {
pkg = fullClsName.substring(0, dot);
name = fullClsName.substring(dot + 1);
}
ClassInfo originClsInfo = originCls.getClassInfo();
String originName = originClsInfo.getShortName();
if (originName.equals(name)
|| name.contains("$")
|| !NameMapper.isValidIdentifier(name)
|| countPkgParts(originClsInfo.getPackage()) != countPkgParts(pkg)
|| pkg.startsWith("java.")) {
return null;
}
ClassNode newClsNode = originCls.root().resolveClass(fullClsName);
if (newClsNode != null) {
// class with alias name already exist
return null;
}
return new ClsAliasPair(pkg, name);
}
private static int countPkgParts(String pkg) {
if (pkg.isEmpty()) {
return 0;
}
int count = 1;
int pos = 0;
while (true) {
pos = pkg.indexOf('.', pos);
if (pos == -1) {
return count;
}
pos++;
count++;
}
}
@SuppressWarnings("unchecked")
private static List<EncodedValue> getParamAsList(IAnnotation annotation, String paramName) {
if (annotation == null) {
return null;
}
EncodedValue encodedValue = annotation.getValues().get(paramName);
if (encodedValue == null || encodedValue.getType() != EncodedType.ENCODED_ARRAY) {
return null;
}
return (List<EncodedValue>) encodedValue.getValue();
}
}

View File

@ -0,0 +1,10 @@
package jadx.core.utils.log
import org.slf4j.Logger
import org.slf4j.LoggerFactory
inline val <reified T : Any> T.LOG: Logger get() = LoggerFactory.getLogger(T::class.java)
inline fun <reified T : Any, R> T.runCatchingLog(msg: String? = null, block: () -> R) =
runCatching(block)
.onFailure { LOG.error(msg.orEmpty(), it) }

View File

@ -1,49 +0,0 @@
package jadx.tests.integration.deobf;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestKotlinMetadata extends SmaliTest {
// @formatter:off
/*
@file:JvmName("TestKotlinMetadata")
class TestMetaData {
@JvmField
val id = 1
@JvmName("makeTwo")
fun double(x: Int): Int {
return 2 * x
}
}
*/
// @formatter:on
@Test
public void test() {
prepareArgs(true);
assertThat(getClassNodeFromSmali())
.code()
.containsOne("class TestMetaData {")
.containsOne("reason: from Kotlin metadata");
}
@Test
public void testIgnoreMetadata() {
prepareArgs(false);
assertThat(getClassNodeFromSmali())
.code()
.containsOne("class C0000TestKotlinMetadata {");
}
private void prepareArgs(boolean parseKotlinMetadata) {
enableDeobfuscation();
args.setDeobfuscationMinLength(100); // rename everything
getArgs().setParseKotlinMetadata(parseKotlinMetadata);
disableCompilation();
}
}

View File

@ -1,73 +0,0 @@
.class public final Ldeobf/TestKotlinMetadata;
.super Ljava/lang/Object;
.source "TestMetaData.kt"
# annotations
.annotation runtime Lkotlin/Metadata;
bv = {
0x1,
0x0,
0x3
}
d1 = {
"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0008\u0002\n\u0002\u0010\u0008\n\u0002\u0008\u0004\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002J\u0015\u0010\u0005\u001a\u00020\u00042\u0006\u0010\u0006\u001a\u00020\u0004H\u0007\u00a2\u0006\u0002\u0008\u0007R\u0010\u0010\u0003\u001a\u00020\u00048\u0006X\u0087D\u00a2\u0006\u0002\n\u0000\u00a8\u0006\u0008"
}
d2 = {
"Ljadx/TestMetaData;",
"",
"()V",
"id",
"",
"double",
"x",
"makeTwo",
"test"
}
k = 0x1
mv = {
0x1,
0x4,
0x0
}
.end annotation
# instance fields
.field public final id:I
.annotation build Lkotlin/jvm/JvmField;
.end annotation
.end field
# direct methods
.method public constructor <init>()V
.registers 2
.prologue
.line 4
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
.line 7
const/4 v0, 0x1
iput v0, p0, Ldeobf/TestKotlinMetadata;->id:I
return-void
.end method
# virtual methods
.method public final makeTwo(I)I
.registers 3
.param p1, "x" # I
.annotation build Lkotlin/jvm/JvmName;
name = "makeTwo"
.end annotation
.prologue
.line 11
mul-int/lit8 v0, p1, 0x2
return v0
.end method

View File

@ -362,10 +362,6 @@ public class JadxSettings extends JadxCLIArgs {
this.deobfuscationUseSourceNameAsAlias = deobfuscationUseSourceNameAsAlias;
}
public void setDeobfuscationParseKotlinMetadata(boolean deobfuscationParseKotlinMetadata) {
this.deobfuscationParseKotlinMetadata = deobfuscationParseKotlinMetadata;
}
public void setUseKotlinMethodsForVarNames(JadxArgs.UseKotlinMethodsForVarNames useKotlinMethodsForVarNames) {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}

View File

@ -322,19 +322,11 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
JCheckBox deobfKotlinMetadata = new JCheckBox();
deobfKotlinMetadata.setSelected(settings.isDeobfuscationParseKotlinMetadata());
deobfKotlinMetadata.addItemListener(e -> {
settings.setDeobfuscationParseKotlinMetadata(e.getStateChange() == ItemEvent.SELECTED);
needReload();
});
SettingsGroup group = new SettingsGroup(NLS.str("preferences.rename"));
group.addRow(NLS.str("preferences.rename_case"), renameCaseSensitive);
group.addRow(NLS.str("preferences.rename_valid"), renameValid);
group.addRow(NLS.str("preferences.rename_printable"), renamePrintable);
group.addRow(NLS.str("preferences.deobfuscation_source_alias"), deobfSourceAlias);
group.addRow(NLS.str("preferences.deobfuscation_kotlin_metadata"), deobfKotlinMetadata);
return group;
}

View File

@ -204,7 +204,6 @@ preferences.generated_renames_mapping_file_mode=Umgang mit Map-Dateien
preferences.deobfuscation_min_len=Minimale Namenlänge
preferences.deobfuscation_max_len=Maximale Namenlänge
preferences.deobfuscation_source_alias=Quelldateiname als Klassennamen-Alias verwenden
preferences.deobfuscation_kotlin_metadata=Kotlin-Metadaten nach Klassen- und Paketnamen analysieren
#preferences.deobfuscation_res_name_source=Better resources name source
preferences.save=Speichern
preferences.cancel=Abbrechen

View File

@ -204,7 +204,6 @@ preferences.generated_renames_mapping_file_mode=Map file handle mode
preferences.deobfuscation_min_len=Minimum name length
preferences.deobfuscation_max_len=Maximum name length
preferences.deobfuscation_source_alias=Use source file name as class name alias
preferences.deobfuscation_kotlin_metadata=Parse Kotlin metadata for class and package names
preferences.deobfuscation_res_name_source=Better resources name source
preferences.save=Save
preferences.cancel=Cancel

View File

@ -204,7 +204,6 @@ preferences.deobfuscation_on=Activar desobfuscación
preferences.deobfuscation_min_len=Longitud mínima del nombre
preferences.deobfuscation_max_len=Longitud máxima del nombre
preferences.deobfuscation_source_alias=Usar el nombre del source como alias para la clase
preferences.deobfuscation_kotlin_metadata=Parse Kotlin metadatos para nombres de clase y paquete
#preferences.deobfuscation_res_name_source=Better resources name source
preferences.save=Guardar
preferences.cancel=Cancelar

View File

@ -204,7 +204,6 @@ preferences.generated_renames_mapping_file_mode=맵 파일 처리 모드
preferences.deobfuscation_min_len=최소 이름 길이
preferences.deobfuscation_max_len=최대 이름 길이
preferences.deobfuscation_source_alias=소스 파일 이름을 클래스 이름 별칭으로 사용
preferences.deobfuscation_kotlin_metadata=클래스 및 패키지 이름에 대한 Kotlin 메타 데이터 파싱
preferences.deobfuscation_res_name_source=더 나은 리소스 이름 소스
preferences.save=저장
preferences.cancel=취소

View File

@ -204,7 +204,6 @@ preferences.deobfuscation_on=Ativar desofuscação
preferences.deobfuscation_min_len=Tamanho mínimo do nome
preferences.deobfuscation_max_len=Tamanho máximo do nome
preferences.deobfuscation_source_alias=Utilizar nome do arquivo como apelido da classe
preferences.deobfuscation_kotlin_metadata=Parsear metadados do kotlin para nome de classes e pacotes
preferences.deobfuscation_res_name_source=Melhora nome da fonte dos recursos
preferences.save=Salvar
preferences.cancel=Cancelar

View File

@ -204,7 +204,6 @@ preferences.deobfuscation_on=Включить деобфускацию
preferences.deobfuscation_min_len=Минимальная длина имени
preferences.deobfuscation_max_len=Максимальная длина имени
preferences.deobfuscation_source_alias=Иcпользовать атрибут SOURCE
preferences.deobfuscation_kotlin_metadata=Использовать метаданные Kotlin
preferences.deobfuscation_res_name_source=Расшифровка имен ресурсов
preferences.save=Сохранить
preferences.cancel=Отмена

View File

@ -204,7 +204,6 @@ preferences.generated_renames_mapping_file_mode=映射文件句柄模式
preferences.deobfuscation_min_len=最小命名长度
preferences.deobfuscation_max_len=最大命名长度
preferences.deobfuscation_source_alias=使用资源名作为类的别名
preferences.deobfuscation_kotlin_metadata=解析Kotlin元数据以获得类名和包名
preferences.deobfuscation_res_name_source=更好的资源名称来源
preferences.save=保存
preferences.cancel=取消

View File

@ -204,7 +204,6 @@ preferences.generated_renames_mapping_file_mode=Map 檔案處理模式
preferences.deobfuscation_min_len=最小名稱長度
preferences.deobfuscation_max_len=最大名稱長度
preferences.deobfuscation_source_alias=將原始檔案名稱作為類別別名
preferences.deobfuscation_kotlin_metadata=剖析 Kotlin 中繼資料來取得類別及套件名稱
preferences.deobfuscation_res_name_source=較佳的資源名稱來源
preferences.save=儲存
preferences.cancel=取消

View File

@ -22,6 +22,7 @@ public class AccessFlags {
public static final int MODULE = 0x8000;
public static final int CONSTRUCTOR = 0x10000;
public static final int DECLARED_SYNCHRONIZED = 0x20000;
public static final int DATA = 0x40000;
public static boolean hasFlag(int flags, int flagValue) {
return (flags & flagValue) != 0;
@ -85,6 +86,9 @@ public class AccessFlags {
if (hasFlag(flags, ENUM)) {
code.append("enum ");
}
if (hasFlag(flags, DATA)) {
code.append("data ");
}
break;
}
if (hasFlag(flags, SYNTHETIC)) {

View File

@ -0,0 +1,14 @@
plugins {
id("jadx-library")
}
dependencies {
api(project(":jadx-core"))
implementation("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.6.0")
testImplementation(project(":jadx-core").dependencyProject.sourceSets.test.get().output)
testImplementation("org.apache.commons:commons-lang3:3.12.0")
testRuntimeOnly(project(":jadx-plugins:jadx-smali-input"))
}

View File

@ -0,0 +1,66 @@
package jadx.plugins.kotlin.metadata
import jadx.api.plugins.options.OptionDescription
import jadx.api.plugins.options.impl.BaseOptionsParser
import jadx.api.plugins.options.impl.JadxOptionDescription
import jadx.plugins.kotlin.metadata.KotlinMetadataPlugin.Companion.PLUGIN_ID
class KotlinMetadataOptions : BaseOptionsParser() {
var isClassAlias: Boolean = true
private set
var isMethodArgs: Boolean = true
private set
var isFields: Boolean = true
private set
var isCompanion: Boolean = true
private set
var isDataClass: Boolean = true
private set
var isToString: Boolean = true
private set
var isGetters: Boolean = true
private set
override fun parseOptions() {
isClassAlias = getBooleanOption(CLASS_ALIAS_OPT, true)
isMethodArgs = getBooleanOption(METHOD_ARGS_OPT, true)
isFields = getBooleanOption(FIELDS_OPT, true)
isCompanion = getBooleanOption(COMPANION_OPT, true)
isDataClass = getBooleanOption(DATA_CLASS_OPT, true)
isToString = getBooleanOption(TO_STRING_OPT, true)
isGetters = getBooleanOption(GETTERS_OPT, true)
}
override fun getOptionsDescriptions(): List<OptionDescription> {
return listOf(
JadxOptionDescription.booleanOption(CLASS_ALIAS_OPT, "rename class alias", true)
.withFlag(OptionDescription.OptionFlag.PER_PROJECT),
JadxOptionDescription.booleanOption(METHOD_ARGS_OPT, "rename function arguments", true)
.withFlag(OptionDescription.OptionFlag.PER_PROJECT),
JadxOptionDescription.booleanOption(FIELDS_OPT, "rename fields", true)
.withFlag(OptionDescription.OptionFlag.PER_PROJECT),
JadxOptionDescription.booleanOption(COMPANION_OPT, "rename companion object", true)
.withFlag(OptionDescription.OptionFlag.PER_PROJECT),
JadxOptionDescription.booleanOption(DATA_CLASS_OPT, "add data class modifier", true)
.withFlag(OptionDescription.OptionFlag.PER_PROJECT),
JadxOptionDescription.booleanOption(TO_STRING_OPT, "rename fields using toString", true)
.withFlag(OptionDescription.OptionFlag.PER_PROJECT),
JadxOptionDescription.booleanOption(GETTERS_OPT, "rename simple getters to field names", true)
.withFlag(OptionDescription.OptionFlag.PER_PROJECT),
)
}
override fun toString(): String {
return "KotlinMetadataOptions(isClassAlias=$isClassAlias, isMethodArgs=$isMethodArgs, isFields=$isFields, isCompanion=$isCompanion, isDataClass=$isDataClass, isToString=$isToString, isGetters=$isGetters)"
}
companion object {
const val CLASS_ALIAS_OPT = "$PLUGIN_ID.class-alias"
const val METHOD_ARGS_OPT = "$PLUGIN_ID.method-args"
const val FIELDS_OPT = "$PLUGIN_ID.fields"
const val COMPANION_OPT = "$PLUGIN_ID.companion"
const val DATA_CLASS_OPT = "$PLUGIN_ID.data-class"
const val TO_STRING_OPT = "$PLUGIN_ID.to-string"
const val GETTERS_OPT = "$PLUGIN_ID.getters"
}
}

View File

@ -0,0 +1,27 @@
package jadx.plugins.kotlin.metadata
import jadx.api.plugins.JadxPlugin
import jadx.api.plugins.JadxPluginContext
import jadx.api.plugins.JadxPluginInfo
import jadx.plugins.kotlin.metadata.pass.KotlinMetadataDecompilePass
import jadx.plugins.kotlin.metadata.pass.KotlinMetadataPreparePass
class KotlinMetadataPlugin : JadxPlugin {
private val options = KotlinMetadataOptions()
override fun getPluginInfo(): JadxPluginInfo {
return JadxPluginInfo(PLUGIN_ID, "Kotlin Metadata", "Use kotlin.Metadata annotation for code generation")
}
override fun init(context: JadxPluginContext) {
context.registerOptions(options)
context.addPass(KotlinMetadataPreparePass(options))
context.addPass(KotlinMetadataDecompilePass(options))
context.registerInputsHashSupplier { options.toString() }
}
companion object {
const val PLUGIN_ID = "kotlin-metadata"
}
}

View File

@ -0,0 +1,12 @@
package jadx.plugins.kotlin.metadata.model
object KotlinMetadataConsts {
const val KOTLIN_METADATA_ANNOTATION = "Lkotlin/Metadata;"
const val KOTLIN_METADATA_K_PARAMETER = "k"
const val KOTLIN_METADATA_D1_PARAMETER = "d1"
const val KOTLIN_METADATA_D2_PARAMETER = "d2"
const val KOTLIN_METADATA_MV_PARAMETER = "mv"
const val KOTLIN_METADATA_XS_PARAMETER = "xs"
const val KOTLIN_METADATA_PN_PARAMETER = "pn"
const val KOTLIN_METADATA_XI_PARAMETER = "xi"
}

View File

@ -0,0 +1,38 @@
package jadx.plugins.kotlin.metadata.model
import jadx.core.dex.instructions.args.RegisterArg
import jadx.core.dex.nodes.ClassNode
import jadx.core.dex.nodes.FieldNode
import jadx.core.dex.nodes.MethodNode
data class ClassAliasRename(
val pkg: String,
val name: String,
)
data class MethodArgRename(
val rArg: RegisterArg,
val alias: String,
)
data class FieldRename(
val field: FieldNode,
val alias: String,
)
data class CompanionRename(
val field: FieldNode,
val cls: ClassNode,
val hide: Boolean,
)
data class ToStringRename(
val cls: ClassNode,
val clsAlias: String?,
val fields: List<FieldRename>,
)
data class MethodRename(
val mth: MethodNode,
val alias: String,
)

View File

@ -0,0 +1,140 @@
package jadx.plugins.kotlin.metadata.pass
import jadx.api.plugins.input.data.AccessFlags
import jadx.api.plugins.pass.JadxPassInfo
import jadx.api.plugins.pass.impl.OrderedJadxPassInfo
import jadx.api.plugins.pass.types.JadxDecompilePass
import jadx.core.dex.attributes.AFlag
import jadx.core.dex.attributes.nodes.RenameReasonAttr
import jadx.core.dex.nodes.ClassNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.dex.nodes.RootNode
import jadx.plugins.kotlin.metadata.KotlinMetadataOptions
import jadx.plugins.kotlin.metadata.utils.KmClassWrapper
import jadx.plugins.kotlin.metadata.utils.KmClassWrapper.Companion.getWrapper
class KotlinMetadataDecompilePass(
private val options: KotlinMetadataOptions,
) : JadxDecompilePass {
override fun getInfo(): JadxPassInfo {
return OrderedJadxPassInfo(
"KotlinMetadataDecompile",
"Use kotlin.Metadata annotation perform various renames",
)
.before("CodeRenameVisitor")
}
override fun init(root: RootNode) {
}
override fun visit(cls: ClassNode): Boolean {
cls.innerClasses.forEach(::visit)
val wrapper = cls.getWrapper() ?: return false
if (options.isMethodArgs) renameMethodArgs(wrapper)
if (options.isFields) renameFields(wrapper)
if (options.isCompanion) renameCompanion(wrapper)
if (options.isDataClass) fixDataClass(wrapper)
if (options.isToString) renameToString(wrapper)
if (options.isGetters) renameGetters(wrapper)
return false
}
override fun visit(mth: MethodNode?) { /* no op */
}
private fun renameMethodArgs(wrapper: KmClassWrapper) {
val args = wrapper.getMethodArgs()
args.forEach { (_, list) ->
list.forEach { (rArg, alias) ->
// TODO comment not being added ?
RenameReasonAttr.forNode(rArg).append(METADATA_REASON)
rArg.name = alias
}
}
}
private fun renameFields(wrapper: KmClassWrapper) {
val fields = wrapper.getFields()
fields.forEach { (field, alias) ->
if (AFlag.DONT_RENAME !in field) {
RenameReasonAttr.forNode(field).append(METADATA_REASON)
field.rename(alias)
}
}
}
private fun renameCompanion(wrapper: KmClassWrapper) {
val companion = wrapper.getCompanion()
companion?.run {
if (AFlag.DONT_RENAME !in field) {
RenameReasonAttr.forNode(field).append(METADATA_REASON)
field.rename(COMPANION_FIELD)
}
if (AFlag.DONT_RENAME !in cls) {
RenameReasonAttr.forNode(cls).append(METADATA_REASON)
cls.rename(COMPANION_CLASS)
}
if (hide) {
field.add(AFlag.DONT_GENERATE)
cls.add(AFlag.DONT_GENERATE)
cls.add(AFlag.DONT_INLINE)
}
}
}
private fun fixDataClass(wrapper: KmClassWrapper) {
val isData = wrapper.isDataClass()
wrapper.cls.run {
if (isData != accessFlags.isData) {
accessFlags = accessFlags.run {
if (isData) {
add(AccessFlags.DATA)
} else {
remove(AccessFlags.DATA)
}
}
}
}
}
private fun renameToString(wrapper: KmClassWrapper) {
val toString = wrapper.parseToString()
toString?.run {
clsAlias?.let { alias ->
if (AFlag.DONT_RENAME !in cls) {
RenameReasonAttr.forNode(cls).append(TO_STRING_REASON)
cls.rename(alias)
}
}
fields.forEach { (field, alias) ->
if (AFlag.DONT_RENAME !in field) {
RenameReasonAttr.forNode(field).append(TO_STRING_REASON)
field.rename(alias)
}
}
}
}
private fun renameGetters(wrapper: KmClassWrapper) {
val getters = wrapper.getGetters()
getters.forEach { (mth, alias) ->
if (AFlag.DONT_RENAME !in mth) {
RenameReasonAttr.forNode(mth).append(GETTER_REASON)
mth.rename(alias)
}
}
}
companion object {
private const val METADATA_REASON = "from kotlin metadata"
private const val COMPANION_FIELD = "INSTANCE"
private const val COMPANION_CLASS = "Companion"
private const val TO_STRING_REASON = "from toString"
private const val GETTER_REASON = "from getter"
}
}

View File

@ -0,0 +1,39 @@
package jadx.plugins.kotlin.metadata.pass
import jadx.api.plugins.pass.JadxPassInfo
import jadx.api.plugins.pass.impl.OrderedJadxPassInfo
import jadx.api.plugins.pass.types.JadxPreparePass
import jadx.core.dex.attributes.AFlag
import jadx.core.dex.nodes.RootNode
import jadx.plugins.kotlin.metadata.KotlinMetadataOptions
import jadx.plugins.kotlin.metadata.utils.KotlinMetadataUtils
class KotlinMetadataPreparePass(
private val options: KotlinMetadataOptions,
) : JadxPreparePass {
override fun getInfo(): JadxPassInfo {
return OrderedJadxPassInfo(
"KotlinMetadataPrepare",
"Use kotlin.Metadata annotation to rename class & package",
)
.before("RenameVisitor")
}
override fun init(root: RootNode) {
if (options.isClassAlias) {
for (cls in root.classes) {
if (cls.contains(AFlag.DONT_RENAME)) {
continue
}
// rename class & package
val kotlinCls = KotlinMetadataUtils.getAlias(cls)
if (kotlinCls != null) {
cls.rename(kotlinCls.name)
cls.packageNode.rename(kotlinCls.pkg)
}
}
}
}
}

View File

@ -0,0 +1,41 @@
package jadx.plugins.kotlin.metadata.utils
import jadx.core.dex.nodes.ClassNode
import kotlinx.metadata.KmClass
import kotlinx.metadata.jvm.KotlinClassMetadata
// don't expose kotlinx.metadata.* types ?
class KmClassWrapper private constructor(
val cls: ClassNode,
private val kmCls: KmClass,
) {
fun getMethodArgs() =
KotlinMetadataUtils.mapMethodArgs(cls, kmCls)
fun getFields() =
KotlinMetadataUtils.mapFields(cls, kmCls)
fun getCompanion() =
KotlinMetadataUtils.mapCompanion(cls, kmCls)
fun isDataClass() =
KotlinUtils.isDataClass(kmCls)
// does not require metadata, may be useful for plain java ?
fun parseToString() =
KotlinUtils.parseToString(cls)
// does not require metadata, may be useful for plain java ?
fun getGetters() =
KotlinUtils.findGetters(cls)
companion object {
fun ClassNode.getWrapper(): KmClassWrapper? {
val metadata = getKotlinClassMetadata()
val kmCls = (metadata as? KotlinClassMetadata.Class)?.toKmClass() ?: return null
return KmClassWrapper(this, kmCls)
}
}
}

View File

@ -0,0 +1,10 @@
package jadx.plugins.kotlin.metadata.utils
import kotlinx.metadata.KmFunction
import kotlinx.metadata.KmProperty
import kotlinx.metadata.jvm.fieldSignature
import kotlinx.metadata.jvm.signature
inline val KmFunction.shortId: String? get() = signature?.asString()
inline val KmProperty.shortId: String? get() = fieldSignature?.asString()

View File

@ -0,0 +1,72 @@
@file:Suppress("UNCHECKED_CAST")
package jadx.plugins.kotlin.metadata.utils
import jadx.api.plugins.input.data.annotations.EncodedType
import jadx.api.plugins.input.data.annotations.EncodedValue
import jadx.api.plugins.input.data.annotations.IAnnotation
import jadx.core.dex.nodes.ClassNode
import jadx.plugins.kotlin.metadata.model.KotlinMetadataConsts
import kotlinx.metadata.jvm.KotlinClassMetadata
import kotlinx.metadata.jvm.Metadata
fun ClassNode.getMetadata(): Metadata? {
val annotation: IAnnotation? = getAnnotation(KotlinMetadataConsts.KOTLIN_METADATA_ANNOTATION)
return annotation?.run {
val k = getParamAsInt(KotlinMetadataConsts.KOTLIN_METADATA_K_PARAMETER)
val mvArray = getParamAsIntArray(KotlinMetadataConsts.KOTLIN_METADATA_MV_PARAMETER)
val d1Array = getParamAsStringArray(KotlinMetadataConsts.KOTLIN_METADATA_D1_PARAMETER)
val d2Array = getParamAsStringArray(KotlinMetadataConsts.KOTLIN_METADATA_D2_PARAMETER)
val xs = getParamAsString(KotlinMetadataConsts.KOTLIN_METADATA_XS_PARAMETER)
val pn = getParamAsString(KotlinMetadataConsts.KOTLIN_METADATA_PN_PARAMETER)
val xi = getParamAsInt(KotlinMetadataConsts.KOTLIN_METADATA_XI_PARAMETER)
Metadata(
kind = k,
metadataVersion = mvArray,
data1 = d1Array,
data2 = d2Array,
extraString = xs,
packageName = pn,
extraInt = xi,
)
}
}
private fun IAnnotation.getParamsAsList(paramName: String): List<EncodedValue>? {
val encodedValue = values[paramName]
?.takeIf { it.type == EncodedType.ENCODED_ARRAY && it.value is List<*> }
return encodedValue?.value?.let { it as List<EncodedValue> }
}
private fun IAnnotation.getParamAsStringArray(paramName: String): Array<String>? {
return getParamsAsList(paramName)
?.map<EncodedValue, Any?>(EncodedValue::getValue)
?.onEach { if (it != null && it !is String) /* TODO is this valid ? */ return@onEach }
?.map { "$it" }
?.toTypedArray()
}
private fun IAnnotation.getParamAsIntArray(paramName: String): IntArray? {
return getParamsAsList(paramName)
?.map<EncodedValue, Any?>(EncodedValue::getValue)
?.map { it as Int }
?.toIntArray()
}
private fun IAnnotation.getParamAsInt(paramName: String): Int? {
val encodedValue = values[paramName]
?.takeIf { it.type == EncodedType.ENCODED_INT && it.value is Int }
return encodedValue?.value?.let { it as Int }
}
private fun IAnnotation.getParamAsString(paramName: String): String? {
val encodedValue = values[paramName]
?.takeIf { it.type == EncodedType.ENCODED_STRING && it.value is String }
return encodedValue?.value?.let { it as String }
}
fun ClassNode.getKotlinClassMetadata(): KotlinClassMetadata? {
return getMetadata()?.let(KotlinClassMetadata::read)
}

View File

@ -0,0 +1,143 @@
package jadx.plugins.kotlin.metadata.utils
import jadx.core.deobf.NameMapper
import jadx.core.dex.attributes.nodes.RenameReasonAttr
import jadx.core.dex.nodes.ClassNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.utils.Utils
import jadx.core.utils.log.LOG
import jadx.plugins.kotlin.metadata.model.ClassAliasRename
import jadx.plugins.kotlin.metadata.model.CompanionRename
import jadx.plugins.kotlin.metadata.model.FieldRename
import jadx.plugins.kotlin.metadata.model.MethodArgRename
import kotlinx.metadata.KmClass
object KotlinMetadataUtils {
@JvmStatic
fun getAlias(cls: ClassNode): ClassAliasRename? {
val annotation = cls.getMetadata() ?: return null
return getClassAlias(cls, annotation)
}
/**
* Try to get class info from Kotlin Metadata annotation
*/
private fun getClassAlias(cls: ClassNode, annotation: Metadata): ClassAliasRename? {
val firstValue = annotation.data2.getOrNull(0) ?: return null
try {
val clsName = firstValue.trim()
.takeUnless(String::isEmpty)
?.let(Utils::cleanObjectName)
?: return null
val alias = splitAndCheckClsName(cls, clsName)
if (alias != null) {
RenameReasonAttr.forNode(cls).append("from Kotlin metadata")
return alias
}
} catch (e: Exception) {
LOG.error("Failed to parse kotlin metadata", e)
}
return null
}
// Don't use ClassInfo facility to not pollute class into cache
private fun splitAndCheckClsName(originCls: ClassNode, fullClsName: String): ClassAliasRename? {
if (!NameMapper.isValidFullIdentifier(fullClsName)) {
return null
}
val pkg: String
val name: String
val dot = fullClsName.lastIndexOf('.')
if (dot == -1) {
pkg = ""
name = fullClsName
} else {
pkg = fullClsName.substring(0, dot)
name = fullClsName.substring(dot + 1)
}
val originClsInfo = originCls.classInfo
val originName = originClsInfo.shortName
if (originName == name || name.contains("$") ||
!NameMapper.isValidIdentifier(name) ||
countPkgParts(originClsInfo.getPackage()) != countPkgParts(pkg) || pkg.startsWith("java.")
) {
return null
}
val newClsNode = originCls.root().resolveClass(fullClsName)
return if (newClsNode != null) {
// class with alias name already exist
null
} else {
ClassAliasRename(pkg, name)
}
}
private fun countPkgParts(pkg: String): Int {
if (pkg.isEmpty()) {
return 0
}
var count = 1
var pos = 0
while (true) {
pos = pkg.indexOf('.', pos)
if (pos == -1) {
return count
}
pos++
count++
}
}
fun mapMethodArgs(cls: ClassNode, kmCls: KmClass): Map<MethodNode, List<MethodArgRename>> {
return buildMap {
kmCls.functions.forEach { kmFunction ->
val node: MethodNode = cls.searchMethodByShortId(kmFunction.shortId) ?: return@forEach
val argCount = node.argTypes.size
val paramCount = kmFunction.valueParameters.size
if (argCount == paramCount) {
// requires arg registers to be loaded, is this necessary ?
val aliasList = node.argRegs.zip(kmFunction.valueParameters).map { (rArg, kmValueParameter) ->
MethodArgRename(rArg = rArg, alias = kmValueParameter.name)
}
put(node, aliasList)
}
}
}
}
fun mapFields(cls: ClassNode, kmCls: KmClass): List<FieldRename> {
return kmCls.properties.mapNotNull { kmProperty ->
val node = cls.searchFieldByShortId(kmProperty.shortId) ?: return@mapNotNull null
FieldRename(field = node, alias = kmProperty.name)
}
}
fun mapCompanion(cls: ClassNode, kmCls: KmClass): CompanionRename? {
val compName = kmCls.companionObject ?: return null
val compField = cls.fields.firstOrNull {
it.name == compName && it.accessFlags.run { isStatic && isFinal && isPublic }
} ?: return null
if (compField.type.isObject) {
val compType = compField.type.`object`
val compCls = cls.innerClasses.firstOrNull {
it.classInfo.makeRawFullName() == compType
} ?: return null
val isOnlyInit = compField.useIn.size == 1 && compField.useIn[0].methodInfo.isClassInit
val isEmpty = compCls.run { methods.all { it.isConstructor } && fields.isEmpty() }
return CompanionRename(
field = compField,
cls = compCls,
hide = isOnlyInit && isEmpty,
)
}
return null
}
}

View File

@ -0,0 +1,96 @@
package jadx.plugins.kotlin.metadata.utils
import jadx.core.Consts
import jadx.core.dex.info.FieldInfo
import jadx.core.dex.instructions.IndexInsnNode
import jadx.core.dex.instructions.InsnType
import jadx.core.dex.instructions.InvokeNode
import jadx.core.dex.instructions.args.PrimitiveType
import jadx.core.dex.nodes.ClassNode
import jadx.core.dex.nodes.FieldNode
import jadx.core.dex.nodes.MethodNode
import jadx.plugins.kotlin.metadata.model.MethodRename
import jadx.plugins.kotlin.metadata.model.ToStringRename
import kotlinx.metadata.Flag
import kotlinx.metadata.KmClass
import java.util.Locale
object KotlinUtils {
fun isDataClass(kmCls: KmClass): Boolean {
return Flag.Class.IS_DATA(kmCls.flags)
}
fun parseToString(cls: ClassNode): ToStringRename? {
val mthToString = cls.searchMethodByShortId(Consts.MTH_TOSTRING_SIGNATURE)
?: return null
return ToStringParser.parse(mthToString)
}
fun findGetters(cls: ClassNode): List<MethodRename> {
return cls.fields.filter(FieldNode::isInstance).mapNotNull { field ->
val mth = getFieldGetterMethod(cls, field.fieldInfo)
?: return@mapNotNull null
MethodRename(
mth = mth,
alias = getGetterAlias(field.alias),
)
}
}
private fun getFieldGetterMethod(cls: ClassNode, field: FieldInfo): MethodNode? {
return cls.methods.firstOrNull {
it.returnType == field.type &&
it.argTypes.isEmpty() &&
it.insnsCount == 3 &&
it.sVars.size == 2 &&
(it.sVars[1].assignInsn as? IndexInsnNode)?.index == field
}
}
private fun getGetterAlias(fieldAlias: String): String {
val capitalized = fieldAlias.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString()
}
return "get$capitalized"
}
// untested & overly complicated
fun parseDefaultMethods(cls: ClassNode): List<MethodRename> {
val possibleMthList = cls.methods.filter {
it.accessFlags.isStatic && it.accessFlags.isSynthetic &&
it.argTypes.run {
size > 3 &&
first().isObject && first().`object` == cls.fullName &&
get(size - 2).isPrimitive && get(size - 2).primitiveType == PrimitiveType.INT &&
last().isObject && last().`object` == Consts.CLASS_OBJECT
}
}
val insnList = possibleMthList.filter {
it.exitBlock.run {
iDom != null && iDom.instructions.firstOrNull()?.type == InsnType.RETURN
iDom.iDom != null
} &&
it.exitBlock.iDom.iDom.run {
instructions.firstOrNull() is InvokeNode
}
}
val remapped = insnList.mapNotNull {
val insn = it.exitBlock.iDom.iDom.instructions.first() as InvokeNode
cls.searchMethodByShortId(insn.callMth.shortId)?.run { it to this }
}
return remapped.map { (defaultMethod, originalMethod) ->
MethodRename(
mth = defaultMethod,
alias = getDefaultMethodAlias(originalMethod.alias),
)
}
}
private fun getDefaultMethodAlias(alias: String): String {
return "$alias\$default"
}
}

View File

@ -0,0 +1,147 @@
package jadx.plugins.kotlin.metadata.utils
import jadx.core.Consts
import jadx.core.dex.info.FieldInfo
import jadx.core.dex.instructions.ConstStringNode
import jadx.core.dex.instructions.IndexInsnNode
import jadx.core.dex.instructions.InsnType
import jadx.core.dex.instructions.InvokeNode
import jadx.core.dex.instructions.InvokeType
import jadx.core.dex.instructions.args.InsnWrapArg
import jadx.core.dex.instructions.args.RegisterArg
import jadx.core.dex.instructions.mods.ConstructorInsn
import jadx.core.dex.nodes.BlockNode
import jadx.core.dex.nodes.InsnNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.utils.BlockUtils
import jadx.core.utils.log.LOG
import jadx.plugins.kotlin.metadata.model.FieldRename
import jadx.plugins.kotlin.metadata.model.ToStringRename
class ToStringParser private constructor(mthToString: MethodNode) {
private var isStarted = false
private var isFirstProcessed = false
private var isFinished = false
private var pendingAlias: String? = null
private var clsAlias: String? = null
private val list: MutableList<Pair<String, FieldInfo>> = mutableListOf()
val isSuccess: Boolean get() = isStarted && isFinished
init {
val blocks: List<BlockNode> = BlockUtils.buildSimplePath(mthToString.enterBlock)
blocks.forEach { block ->
block.instructions.forEach { insn ->
process(insn)
}
}
}
private fun process(insn: InsnNode) {
if (!isStarted) {
isStarted = isStartStringBuilder(insn)
return
}
if (isFinished) {
return
}
if (isAppendInvoke(insn)) {
val arg = insn.getArg(1)
// invoke with const string
if (arg.isInsnWrap && arg is InsnWrapArg && arg.wrapInsn.type == InsnType.CONST_STR) {
val constStr: String? = (arg.wrapInsn as ConstStringNode).string
handleString(requireNotNull(constStr) { "Failed to get const String" })
}
// invoke with register
if (arg.isRegister && arg is RegisterArg) {
val assign = arg.sVar.assignInsn
// basic argument
if (assign is IndexInsnNode) {
val info: FieldInfo? = (arg.sVar.assignInsn as IndexInsnNode).index as? FieldInfo
handleFieldInfo(requireNotNull(info) { "Failed to get FieldInfo from index" })
}
// string formatted argument, for rare cases like Arrays.toString(...)
if (assign is InvokeNode && assign.invokeType == InvokeType.STATIC && assign.argsCount == 1) {
val prevArg = assign.getArg(0)
if (prevArg.isRegister && prevArg is RegisterArg) {
if (prevArg.sVar.assignInsn is IndexInsnNode) {
val info: FieldInfo? = (prevArg.sVar.assignInsn as IndexInsnNode).index as? FieldInfo
handleFieldInfo(requireNotNull(info) { "Failed to get nested FieldInfo from index" })
}
}
}
}
return
}
isFinished = isToString(insn)
}
private fun handleString(string: String) {
if (pendingAlias != null) {
LOG.warn("Skipping pending alias: '$pendingAlias'")
}
if (!isFirstProcessed) {
clsAlias = string.substringBefore('(')
pendingAlias = string
.substringAfter('(')
.substringBeforeLast('=')
isFirstProcessed = true
} else {
pendingAlias = string
.substringAfter(", ")
.substringBeforeLast('=')
}
}
private fun handleFieldInfo(fieldInfo: FieldInfo) {
list.add(requireNotNull(pendingAlias) { "No pending alias found" } to fieldInfo)
pendingAlias = null
}
companion object {
fun parse(mth: MethodNode): ToStringRename? {
val parser =
kotlin.runCatching { ToStringParser(mth) }.getOrNull()
if (parser?.isSuccess != true) return null
val cls = mth.parentClass
return ToStringRename(
cls = cls,
clsAlias = parser.clsAlias,
fields = parser.list.mapNotNull { (alias, fieldInfo) ->
val field = cls.searchField(fieldInfo)
?: return@mapNotNull null
FieldRename(
field = field,
alias = alias,
)
},
)
}
private fun isStartStringBuilder(inst: InsnNode): Boolean {
return inst is ConstructorInsn &&
inst.isNewInstance &&
inst.callMth.declClass.fullName == Consts.CLASS_STRING_BUILDER
}
private fun isAppendInvoke(inst: InsnNode): Boolean {
return inst is InvokeNode &&
inst.callMth.declClass.fullName == Consts.CLASS_STRING_BUILDER &&
inst.callMth.name == "append" &&
inst.argsCount == 2
}
private fun isToString(inst: InsnNode): Boolean {
return inst is InvokeNode &&
inst.callMth.declClass.fullName == Consts.CLASS_STRING_BUILDER &&
inst.callMth.shortId == Consts.MTH_TOSTRING_SIGNATURE
}
}
}

View File

@ -0,0 +1 @@
jadx.plugins.kotlin.metadata.KotlinMetadataPlugin

View File

@ -0,0 +1,165 @@
package jadx.plugins.kotlin.metadata.tests
import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.CLASS_ALIAS_OPT
import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.COMPANION_OPT
import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.DATA_CLASS_OPT
import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.FIELDS_OPT
import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.GETTERS_OPT
import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.METHOD_ARGS_OPT
import jadx.plugins.kotlin.metadata.KotlinMetadataOptions.Companion.TO_STRING_OPT
import jadx.tests.api.SmaliTest
import jadx.tests.api.utils.assertj.JadxAssertions.assertThat
import jadx.tests.api.utils.assertj.JadxCodeAssertions
import org.junit.jupiter.api.Test
class TestKotlinMetadata : SmaliTest() {
// @formatter:off
/*
package deobf
data class DataClassSample(
val name: String,
private val id: Int,
) {
var inner: Short = 3
companion object {
fun getTag(): String {
return "TAG"
}
}
}
*/
// @formatter:on
@Test
fun testMethodArgs() {
setupArgs { this[METHOD_ARGS_OPT] = true }
assertThatClass()
.containsOne("public boolean equals(Object other) {")
}
@Test
fun testIgnoreMethodArgs() {
setupArgs()
assertThatClass()
.containsOne("public boolean equals(Object obj) {")
}
@Test
fun testFields() {
setupArgs { this[FIELDS_OPT] = true }
assertThatClass()
.containsOne("private final String name;")
.containsOne("private final int id;")
.containsOne("private short inner;")
.countString(3, "reason: from kotlin metadata")
}
@Test
fun testIgnoreFields() {
setupArgs()
assertThatClass()
.containsOne("private final String a;")
.containsOne("private final int b;")
.containsOne("private short c;")
.countString(0, "reason: from kotlin metadata")
}
@Test
fun testCompanion() {
setupArgs { this[COMPANION_OPT] = true }
assertThatClass()
.containsOne("public static final Companion INSTANCE = new Companion(null);")
.containsOne("public static final class Companion {")
.countString(2, "reason: from kotlin metadata")
}
@Test
fun testIgnoreCompanion() {
setupArgs()
assertThatClass()
.containsOne("public static final b d = new b(null);")
.containsOne("public static final class b {")
.countString(0, "reason: from kotlin metadata")
}
@Test
fun testDataClass() {
setupArgs { this[DATA_CLASS_OPT] = true }
assertThatClass()
.containsOne("/* data */")
}
@Test
fun testIgnoreDataClass() {
setupArgs()
assertThatClass()
.countString(0, "/* data */")
}
@Test
fun testToString() {
setupArgs { this[TO_STRING_OPT] = true }
assertThatClass()
.containsOne("public final class DataClassSample {")
.containsOne("private final String name;")
.containsOne("private final int id;")
.countString(3, "reason: from toString")
}
@Test
fun testIgnoreToString() {
setupArgs()
assertThatClass()
.containsOne("public final class a {")
.containsOne("private final String a;")
.containsOne("private final int b;")
.countString(0, "reason: from toString")
}
@Test
fun testGetters() {
setupArgs { this[GETTERS_OPT] = true }
assertThatClass()
.containsOne("public final String getA() {")
.countString(1, "reason: from getter")
}
@Test
fun testGettersAlias() {
setupArgs {
this[FIELDS_OPT] = true
this[GETTERS_OPT] = true
}
assertThatClass()
.containsOne("public final String getName() {")
.countString(1, "reason: from getter")
}
@Test
fun testIgnoreGetters() {
setupArgs()
assertThatClass()
.countString(0, "reason: from getter")
}
private fun setupArgs(builder: MutableMap<String, Boolean>.() -> Unit = {}) {
val allOff = mutableMapOf(
CLASS_ALIAS_OPT to false,
METHOD_ARGS_OPT to false,
FIELDS_OPT to false,
COMPANION_OPT to false,
DATA_CLASS_OPT to false,
TO_STRING_OPT to false,
GETTERS_OPT to false,
)
args.pluginOptions = allOff.apply(builder).mapValues {
if (it.value) "yes" else "no"
}
}
private fun assertThatClass(): JadxCodeAssertions =
assertThat(getClassNodeFromSmaliFiles("deobf", "TestKotlinMetadata", "a"))
.code()
}

View File

@ -0,0 +1,65 @@
.class public final Ldeobf/a$b;
.super Ljava/lang/Object;
.source "SourceFile"
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Ldeobf/a;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "b"
.end annotation
.annotation runtime Lkotlin/Metadata;
d1 = {
"\u0000\u0010\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0010\u000e\n\u0002\u0008\u0004\u0008\u0086\u0003\u0018\u00002\u00020\u0001B\t\u0008\u0002\u00a2\u0006\u0004\u0008\u0004\u0010\u0005J\u0006\u0010\u0003\u001a\u00020\u0002\u00a8\u0006\u0006"
}
d2 = {
"Ldeobf/DataClassSample$Companion;",
"",
"",
"a",
"<init>",
"()V",
"app_release"
}
k = 0x1
mv = {
0x1,
0x8,
0x0
}
.end annotation
# direct methods
.method private constructor <init>()V
.registers 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public synthetic constructor <init>(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
.registers 2
.line 1
invoke-direct {p0}, Ldeobf/a$b;-><init>()V
return-void
.end method
# virtual methods
.method public final a()Ljava/lang/String;
.registers 2
const-string v0, "TAG"
return-object v0
.end method

View File

@ -0,0 +1,216 @@
.class public final Ldeobf/a;
.super Ljava/lang/Object;
.source "SourceFile"
# annotations
.annotation system Ldalvik/annotation/MemberClasses;
value = {
Ldeobf/a$b;
}
.end annotation
.annotation runtime Lkotlin/Metadata;
d1 = {
"\u0000&\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0010\u0008\n\u0002\u0008\u0002\n\u0002\u0010\u000b\n\u0002\u0008\u0008\n\u0002\u0010\n\n\u0002\u0008\u000b\u0008\u0086\u0008\u0018\u0000 \u00192\u00020\u0001:\u0001\u001aB\u0017\u0012\u0006\u0010\u000c\u001a\u00020\u0002\u0012\u0006\u0010\u000f\u001a\u00020\u0004\u00a2\u0006\u0004\u0008\u0017\u0010\u0018J\t\u0010\u0003\u001a\u00020\u0002H\u00d6\u0001J\t\u0010\u0005\u001a\u00020\u0004H\u00d6\u0001J\u0013\u0010\u0008\u001a\u00020\u00072\u0008\u0010\u0006\u001a\u0004\u0018\u00010\u0001H\u00d6\u0003R\u0017\u0010\u000c\u001a\u00020\u00028\u0006\u00a2\u0006\u000c\n\u0004\u0008\t\u0010\n\u001a\u0004\u0008\t\u0010\u000bR\u0014\u0010\u000f\u001a\u00020\u00048\u0002X\u0082\u0004\u00a2\u0006\u0006\n\u0004\u0008\r\u0010\u000eR\"\u0010\u0016\u001a\u00020\u00108\u0006@\u0006X\u0086\u000e\u00a2\u0006\u0012\n\u0004\u0008\u0011\u0010\u0012\u001a\u0004\u0008\u0013\u0010\u0014\"\u0004\u0008\r\u0010\u0015\u00a8\u0006\u001b"
}
d2 = {
"Ldeobf/DataClassSample;",
"",
"",
"toString",
"",
"hashCode",
"other",
"",
"equals",
"a",
"Ljava/lang/String;",
"()Ljava/lang/String;",
"name",
"b",
"I",
"id",
"",
"c",
"S",
"getInner",
"()S",
"(S)V",
"inner",
"<init>",
"(Ljava/lang/String;I)V",
"d",
"Companion",
"app_release"
}
k = 0x1
mv = {
0x1,
0x8,
0x0
}
.end annotation
# static fields
.field public static final d:Ldeobf/a$b;
# instance fields
.field private final a:Ljava/lang/String;
.field private final b:I
.field private c:S
# direct methods
.method static constructor <clinit>()V
.registers 2
new-instance v0, Ldeobf/a$b;
const/4 v1, 0x0
invoke-direct {v0, v1}, Ldeobf/a$b;-><init>(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
sput-object v0, Ldeobf/a;->d:Ldeobf/a$b;
return-void
.end method
.method public constructor <init>(Ljava/lang/String;I)V
.registers 4
const-string v0, "name"
invoke-static {p1, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Ldeobf/a;->a:Ljava/lang/String;
iput p2, p0, Ldeobf/a;->b:I
const/4 p1, 0x3
iput-short p1, p0, Ldeobf/a;->c:S
return-void
.end method
# virtual methods
.method public final a()Ljava/lang/String;
.registers 2
iget-object v0, p0, Ldeobf/a;->a:Ljava/lang/String;
return-object v0
.end method
.method public final b(S)V
.registers 2
iput-short p1, p0, Ldeobf/a;->c:S
return-void
.end method
.method public equals(Ljava/lang/Object;)Z
.registers 6
const/4 v0, 0x1
if-ne p0, p1, :cond_4
return v0
:cond_4
instance-of v1, p1, Ldeobf/a;
const/4 v2, 0x0
if-nez v1, :cond_a
return v2
:cond_a
check-cast p1, Ldeobf/a;
iget-object v1, p0, Ldeobf/a;->a:Ljava/lang/String;
iget-object v3, p1, Ldeobf/a;->a:Ljava/lang/String;
invoke-static {v1, v3}, Lkotlin/jvm/internal/Intrinsics;->areEqual(Ljava/lang/Object;Ljava/lang/Object;)Z
move-result v1
if-nez v1, :cond_17
return v2
:cond_17
iget v1, p0, Ldeobf/a;->b:I
iget p1, p1, Ldeobf/a;->b:I
if-eq v1, p1, :cond_1e
return v2
:cond_1e
return v0
.end method
.method public hashCode()I
.registers 3
iget-object v0, p0, Ldeobf/a;->a:Ljava/lang/String;
invoke-virtual {v0}, Ljava/lang/String;->hashCode()I
move-result v0
mul-int/lit8 v0, v0, 0x1f
iget v1, p0, Ldeobf/a;->b:I
add-int/2addr v0, v1
return v0
.end method
.method public toString()Ljava/lang/String;
.registers 5
iget-object v0, p0, Ldeobf/a;->a:Ljava/lang/String;
iget v1, p0, Ldeobf/a;->b:I
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
const-string v3, "DataClassSample(name="
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v0, ", id="
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
const-string v0, ")"
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
return-object v0
.end method

View File

@ -15,6 +15,7 @@ include("jadx-plugins:jadx-raung-input")
include("jadx-plugins:jadx-smali-input")
include("jadx-plugins:jadx-java-convert")
include("jadx-plugins:jadx-rename-mappings")
include("jadx-plugins:jadx-kotlin-metadata")
include("jadx-plugins:jadx-script:jadx-script-plugin")
include("jadx-plugins:jadx-script:jadx-script-runtime")