From ccdbb1d62c9650fa16016d4ec0f3a5880c5e6c1b Mon Sep 17 00:00:00 2001 From: Krzysztof Iwaniuk Date: Sun, 14 May 2023 22:34:40 +0200 Subject: [PATCH] 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 --- README.md | 11 + jadx-cli/build.gradle | 2 + .../src/main/java/jadx/cli/JadxCLIArgs.java | 8 - jadx-core/build.gradle | 1 - .../src/main/java/jadx/api/JadxArgs.java | 12 +- .../java/jadx/core/dex/info/AccessInfo.java | 7 + .../visitors/rename/KotlinMetadataRename.java | 25 -- .../dex/visitors/rename/RenameVisitor.java | 1 - .../jadx/core/utils/kotlin/ClsAliasPair.java | 24 -- .../utils/kotlin/KotlinMetadataUtils.java | 115 ---------- .../main/java/jadx/core/utils/log/LogExt.kt | 10 + .../integration/deobf/TestKotlinMetadata.java | 49 ---- .../test/smali/deobf/TestKotlinMetadata.smali | 73 ------ .../java/jadx/gui/settings/JadxSettings.java | 4 - .../jadx/gui/settings/JadxSettingsWindow.java | 8 - .../resources/i18n/Messages_de_DE.properties | 1 - .../resources/i18n/Messages_en_US.properties | 1 - .../resources/i18n/Messages_es_ES.properties | 1 - .../resources/i18n/Messages_ko_KR.properties | 1 - .../resources/i18n/Messages_pt_BR.properties | 1 - .../resources/i18n/Messages_ru_RU.properties | 1 - .../resources/i18n/Messages_zh_CN.properties | 1 - .../resources/i18n/Messages_zh_TW.properties | 1 - .../api/plugins/input/data/AccessFlags.java | 4 + .../jadx-kotlin-metadata/build.gradle.kts | 14 ++ .../kotlin/metadata/KotlinMetadataOptions.kt | 66 ++++++ .../kotlin/metadata/KotlinMetadataPlugin.kt | 27 +++ .../metadata/model/KotlinMetadataConsts.kt | 12 + .../metadata/model/KotlinRenameResults.kt | 38 +++ .../pass/KotlinMetadataDecompilePass.kt | 140 ++++++++++++ .../pass/KotlinMetadataPreparePass.kt | 39 ++++ .../kotlin/metadata/utils/KmClassWrapper.kt | 41 ++++ .../plugins/kotlin/metadata/utils/KmExt.kt | 10 + .../metadata/utils/KotlinMetadataExt.kt | 72 ++++++ .../metadata/utils/KotlinMetadataUtils.kt | 143 ++++++++++++ .../kotlin/metadata/utils/KotlinUtils.kt | 96 ++++++++ .../kotlin/metadata/utils/ToStringParser.kt | 147 ++++++++++++ .../services/jadx.api.plugins.JadxPlugin | 1 + .../src/test/kotlin/TestKotlinMetadata.kt | 165 +++++++++++++ .../smali/deobf/TestKotlinMetadata/a$b.smali | 65 ++++++ .../smali/deobf/TestKotlinMetadata/a.smali | 216 ++++++++++++++++++ settings.gradle.kts | 1 + 42 files changed, 1328 insertions(+), 327 deletions(-) delete mode 100644 jadx-core/src/main/java/jadx/core/dex/visitors/rename/KotlinMetadataRename.java delete mode 100644 jadx-core/src/main/java/jadx/core/utils/kotlin/ClsAliasPair.java delete mode 100644 jadx-core/src/main/java/jadx/core/utils/kotlin/KotlinMetadataUtils.java create mode 100644 jadx-core/src/main/java/jadx/core/utils/log/LogExt.kt delete mode 100644 jadx-core/src/test/java/jadx/tests/integration/deobf/TestKotlinMetadata.java delete mode 100644 jadx-core/src/test/smali/deobf/TestKotlinMetadata.smali create mode 100644 jadx-plugins/jadx-kotlin-metadata/build.gradle.kts create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataPlugin.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinMetadataConsts.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinRenameResults.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataDecompilePass.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataPreparePass.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmClassWrapper.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmExt.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataExt.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataUtils.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinUtils.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/ToStringParser.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/test/kotlin/TestKotlinMetadata.kt create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a$b.smali create mode 100644 jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a.smali diff --git a/README.md b/README.md index 6c3ada36c..4f5e89bd4 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,17 @@ Plugin options (-P=): 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 diff --git a/jadx-cli/build.gradle b/jadx-cli/build.gradle index 7863d756d..4bd54e7e7 100644 --- a/jadx-cli/build.gradle +++ b/jadx-cli/build.gradle @@ -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' diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index a6146ab2e..843c2b1de 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -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; } diff --git a/jadx-core/build.gradle b/jadx-core/build.gradle index d69ab8bfd..ab759831f 100644 --- a/jadx-core/build.gradle +++ b/jadx-core/build.gradle @@ -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' diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index abeb8406f..220f5700a 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -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 diff --git a/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java b/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java index 864d95b00..369e745c2 100644 --- a/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java +++ b/jadx-core/src/main/java/jadx/core/dex/info/AccessInfo.java @@ -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 */ "); } diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/KotlinMetadataRename.java b/jadx-core/src/main/java/jadx/core/dex/visitors/rename/KotlinMetadataRename.java deleted file mode 100644 index 716e7a609..000000000 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/KotlinMetadataRename.java +++ /dev/null @@ -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()); - } - } - } - } -} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java index 6f22c022b..68b89c867 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/rename/RenameVisitor.java @@ -40,7 +40,6 @@ public class RenameVisitor extends AbstractVisitor { } private void process(RootNode root) { - KotlinMetadataRename.process(root); SourceFileRename.process(root); UserRenames.apply(root); diff --git a/jadx-core/src/main/java/jadx/core/utils/kotlin/ClsAliasPair.java b/jadx-core/src/main/java/jadx/core/utils/kotlin/ClsAliasPair.java deleted file mode 100644 index 3d7fe9f20..000000000 --- a/jadx-core/src/main/java/jadx/core/utils/kotlin/ClsAliasPair.java +++ /dev/null @@ -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; - } -} diff --git a/jadx-core/src/main/java/jadx/core/utils/kotlin/KotlinMetadataUtils.java b/jadx-core/src/main/java/jadx/core/utils/kotlin/KotlinMetadataUtils.java deleted file mode 100644 index fa05dc305..000000000 --- a/jadx-core/src/main/java/jadx/core/utils/kotlin/KotlinMetadataUtils.java +++ /dev/null @@ -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 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 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.getValue(); - } -} diff --git a/jadx-core/src/main/java/jadx/core/utils/log/LogExt.kt b/jadx-core/src/main/java/jadx/core/utils/log/LogExt.kt new file mode 100644 index 000000000..c12180c9a --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/utils/log/LogExt.kt @@ -0,0 +1,10 @@ +package jadx.core.utils.log + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +inline val T.LOG: Logger get() = LoggerFactory.getLogger(T::class.java) + +inline fun T.runCatchingLog(msg: String? = null, block: () -> R) = + runCatching(block) + .onFailure { LOG.error(msg.orEmpty(), it) } diff --git a/jadx-core/src/test/java/jadx/tests/integration/deobf/TestKotlinMetadata.java b/jadx-core/src/test/java/jadx/tests/integration/deobf/TestKotlinMetadata.java deleted file mode 100644 index f08fc0705..000000000 --- a/jadx-core/src/test/java/jadx/tests/integration/deobf/TestKotlinMetadata.java +++ /dev/null @@ -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(); - } -} diff --git a/jadx-core/src/test/smali/deobf/TestKotlinMetadata.smali b/jadx-core/src/test/smali/deobf/TestKotlinMetadata.smali deleted file mode 100644 index 58e66da7d..000000000 --- a/jadx-core/src/test/smali/deobf/TestKotlinMetadata.smali +++ /dev/null @@ -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 ()V - .registers 2 - - .prologue - .line 4 - invoke-direct {p0}, Ljava/lang/Object;->()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 diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 5deecbcc1..21b1f0eb9 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -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; } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index f62f4f54d..d7494de2a 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -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; } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 3e31953c9..da04c715d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index bdfeb0996..9b2c1bac9 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 463112315..989a765d1 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 888b08731..f07a8a576 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -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=취소 diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index 6f3334387..3176a0dfe 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -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 diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 0781bf96a..79c030db8 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -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=Отмена diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 81493875e..c0da897ef 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -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=取消 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index f553c3b38..377efcb44 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -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=取消 diff --git a/jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/AccessFlags.java b/jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/AccessFlags.java index 8634282ff..5e821a4fb 100644 --- a/jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/AccessFlags.java +++ b/jadx-plugins/jadx-input-api/src/main/java/jadx/api/plugins/input/data/AccessFlags.java @@ -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)) { diff --git a/jadx-plugins/jadx-kotlin-metadata/build.gradle.kts b/jadx-plugins/jadx-kotlin-metadata/build.gradle.kts new file mode 100644 index 000000000..190692794 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/build.gradle.kts @@ -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")) +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt new file mode 100644 index 000000000..e12b322b9 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataOptions.kt @@ -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 { + 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" + } +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataPlugin.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataPlugin.kt new file mode 100644 index 000000000..aedf0b615 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/KotlinMetadataPlugin.kt @@ -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" + } +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinMetadataConsts.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinMetadataConsts.kt new file mode 100644 index 000000000..714a62ceb --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinMetadataConsts.kt @@ -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" +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinRenameResults.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinRenameResults.kt new file mode 100644 index 000000000..75a7db8d7 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/model/KotlinRenameResults.kt @@ -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, +) + +data class MethodRename( + val mth: MethodNode, + val alias: String, +) diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataDecompilePass.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataDecompilePass.kt new file mode 100644 index 000000000..d3d058490 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataDecompilePass.kt @@ -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" + } +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataPreparePass.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataPreparePass.kt new file mode 100644 index 000000000..285c82811 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/pass/KotlinMetadataPreparePass.kt @@ -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) + } + } + } + } +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmClassWrapper.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmClassWrapper.kt new file mode 100644 index 000000000..550db5314 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmClassWrapper.kt @@ -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) + } + } +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmExt.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmExt.kt new file mode 100644 index 000000000..c7b7c3d83 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KmExt.kt @@ -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() diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataExt.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataExt.kt new file mode 100644 index 000000000..64c8648aa --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataExt.kt @@ -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? { + val encodedValue = values[paramName] + ?.takeIf { it.type == EncodedType.ENCODED_ARRAY && it.value is List<*> } + return encodedValue?.value?.let { it as List } +} + +private fun IAnnotation.getParamAsStringArray(paramName: String): Array? { + return getParamsAsList(paramName) + ?.map(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::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) +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataUtils.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataUtils.kt new file mode 100644 index 000000000..38da2d469 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinMetadataUtils.kt @@ -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> { + 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 { + 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 + } +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinUtils.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinUtils.kt new file mode 100644 index 000000000..bf71d6556 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/KotlinUtils.kt @@ -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 { + 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 { + 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" + } +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/ToStringParser.kt b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/ToStringParser.kt new file mode 100644 index 000000000..ee40bf894 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/kotlin/jadx/plugins/kotlin/metadata/utils/ToStringParser.kt @@ -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> = mutableListOf() + val isSuccess: Boolean get() = isStarted && isFinished + + init { + val blocks: List = 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 + } + } +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin b/jadx-plugins/jadx-kotlin-metadata/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin new file mode 100644 index 000000000..3571940ca --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/main/resources/META-INF/services/jadx.api.plugins.JadxPlugin @@ -0,0 +1 @@ +jadx.plugins.kotlin.metadata.KotlinMetadataPlugin diff --git a/jadx-plugins/jadx-kotlin-metadata/src/test/kotlin/TestKotlinMetadata.kt b/jadx-plugins/jadx-kotlin-metadata/src/test/kotlin/TestKotlinMetadata.kt new file mode 100644 index 000000000..5164b8201 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/test/kotlin/TestKotlinMetadata.kt @@ -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.() -> 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() +} diff --git a/jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a$b.smali b/jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a$b.smali new file mode 100644 index 000000000..54093df42 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a$b.smali @@ -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", + "", + "()V", + "app_release" + } + k = 0x1 + mv = { + 0x1, + 0x8, + 0x0 + } +.end annotation + + +# direct methods +.method private constructor ()V + .registers 1 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + +.method public synthetic constructor (Lkotlin/jvm/internal/DefaultConstructorMarker;)V + .registers 2 + + .line 1 + invoke-direct {p0}, Ldeobf/a$b;->()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 diff --git a/jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a.smali b/jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a.smali new file mode 100644 index 000000000..d21a19ce0 --- /dev/null +++ b/jadx-plugins/jadx-kotlin-metadata/src/test/smali/deobf/TestKotlinMetadata/a.smali @@ -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", + "", + "(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 ()V + .registers 2 + + new-instance v0, Ldeobf/a$b; + + const/4 v1, 0x0 + + invoke-direct {v0, v1}, Ldeobf/a$b;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V + + sput-object v0, Ldeobf/a;->d:Ldeobf/a$b; + + return-void +.end method + +.method public constructor (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;->()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;->()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 diff --git a/settings.gradle.kts b/settings.gradle.kts index 77b20eab7..a8d050466 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")