From 3814951408757d975cd8122d932307bc4555375c Mon Sep 17 00:00:00 2001 From: pubiqq <82187521+pubiqq@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:01:57 +0300 Subject: [PATCH] feat: improve `better name` algorithm (PR #2299) --- .../dex/visitors/rename/SourceFileRename.java | 2 +- .../main/java/jadx/core/utils/BetterName.java | 78 +++++++++++++++++++ .../core/xmlgen/ResTableBinaryParser.java | 2 +- .../java/jadx/core/utils/TestBetterName.java | 2 + .../core/utils/TestGetBetterClassName.java | 67 ++++++++++++++++ .../core/utils/TestGetBetterResourceName.java | 37 +++++++++ .../rename/TestUsingSourceFileName.java | 35 ++++++++- 7 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 jadx-core/src/test/java/jadx/core/utils/TestGetBetterClassName.java create mode 100644 jadx-core/src/test/java/jadx/core/utils/TestGetBetterResourceName.java diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/SourceFileRename.java b/jadx-core/src/main/java/jadx/core/dex/visitors/rename/SourceFileRename.java index 8084dde82..a6f745fc0 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/rename/SourceFileRename.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/rename/SourceFileRename.java @@ -81,7 +81,7 @@ public class SourceFileRename extends AbstractVisitor { case ALWAYS: return sourceName; case IF_BETTER: - return BetterName.compareAndGet(sourceName, currentName); + return BetterName.getBetterClassName(sourceName, currentName); case NEVER: return currentName; default: diff --git a/jadx-core/src/main/java/jadx/core/utils/BetterName.java b/jadx-core/src/main/java/jadx/core/utils/BetterName.java index d0c292861..fd0531b21 100644 --- a/jadx-core/src/main/java/jadx/core/utils/BetterName.java +++ b/jadx-core/src/main/java/jadx/core/utils/BetterName.java @@ -15,6 +15,78 @@ public class BetterName { private static final boolean DEBUG = false; + private static final double TOLERANCE = 0.001; + + /** + * Compares two class names and returns the "better" one. + * If both names are equally good, {@code firstName} is returned. + */ + public static String getBetterClassName(String firstName, String secondName) { + return getBetterName(firstName, secondName); + } + + /** + * Compares two resource names and returns the "better" one. + * If both names are equally good, {@code firstName} is returned. + */ + public static String getBetterResourceName(String firstName, String secondName) { + return getBetterName(firstName, secondName); + } + + private static String getBetterName(String firstName, String secondName) { + if (Objects.equals(firstName, secondName)) { + return firstName; + } + + if (StringUtils.isEmpty(firstName) || StringUtils.isEmpty(secondName)) { + return StringUtils.notEmpty(firstName) + ? firstName + : secondName; + } + + final var firstResult = analyze(firstName); + final var secondResult = analyze(secondName); + + if (firstResult.digitCount != 0 || secondResult.digitCount != 0) { + final var firstRatio = (float) firstResult.digitCount / firstResult.length; + final var secondRatio = (float) secondResult.digitCount / secondResult.length; + + if (Math.abs(secondRatio - firstRatio) >= TOLERANCE) { + return firstRatio <= secondRatio + ? firstName + : secondName; + } + } + + return firstResult.length >= secondResult.length + ? firstName + : secondName; + } + + private static AnalyzeResult analyze(String name) { + final var result = new AnalyzeResult(); + + StringUtils.visitCodePoints(name, cp -> { + if (Character.isDigit(cp)) { + result.digitCount++; + } + + result.length++; + }); + + return result; + } + + private static class AnalyzeResult { + private int length; + private int digitCount; + } + + /** + * @deprecated Use {@link #getBetterClassName(String, String)} or + * {@link #getBetterResourceName(String, String)} instead. + */ + @Deprecated public static String compareAndGet(String first, String second) { if (Objects.equals(first, second)) { return first; @@ -32,6 +104,11 @@ public class BetterName { return firstBetter ? first : second; } + /** + * @deprecated This function is an implementation detail of deprecated + * {@link #compareAndGet(String, String)} and should not be used outside tests. + */ + @Deprecated public static int calcRating(String str) { int rating = str.length() * 3; rating += differentCharsCount(str) * 20; @@ -49,6 +126,7 @@ public class BetterName { return rating; } + @Deprecated private static int differentCharsCount(String str) { String lower = str.toLowerCase(Locale.ROOT); Set chars = new HashSet<>(); diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java index 7a0019748..604c1988a 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResTableBinaryParser.java @@ -490,7 +490,7 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl public static String getBetterName(ResourceNameSource nameSource, String resName, String codeName) { switch (nameSource) { case AUTO: - return BetterName.compareAndGet(resName, codeName); + return BetterName.getBetterResourceName(resName, codeName); case RESOURCES: return resName; case CODE: diff --git a/jadx-core/src/test/java/jadx/core/utils/TestBetterName.java b/jadx-core/src/test/java/jadx/core/utils/TestBetterName.java index acd747106..3b3c1b13f 100644 --- a/jadx-core/src/test/java/jadx/core/utils/TestBetterName.java +++ b/jadx-core/src/test/java/jadx/core/utils/TestBetterName.java @@ -7,12 +7,14 @@ import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestBetterName { + @Deprecated @Test public void test() { expectFirst("color_main", "t0"); expectFirst("done", "oOo0oO0o"); } + @Deprecated private void expectFirst(String first, String second) { String best = BetterName.compareAndGet(first, second); assertThat(best) diff --git a/jadx-core/src/test/java/jadx/core/utils/TestGetBetterClassName.java b/jadx-core/src/test/java/jadx/core/utils/TestGetBetterClassName.java new file mode 100644 index 000000000..60899203d --- /dev/null +++ b/jadx-core/src/test/java/jadx/core/utils/TestGetBetterClassName.java @@ -0,0 +1,67 @@ +package jadx.core.utils; + +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.Test; + +import static jadx.core.utils.BetterName.getBetterClassName; +import static org.assertj.core.api.Assertions.assertThat; + +public class TestGetBetterClassName { + + @Test + public void testGoodNamesVsGeneratedAliases() { + assertThatBetterClassName("AppCompatButton", "C2404e2").isEqualTo("AppCompatButton"); + assertThatBetterClassName("ContextThemeWrapper", "C2106b1").isEqualTo("ContextThemeWrapper"); + assertThatBetterClassName("ListPopupWindow", "C2344a3").isEqualTo("ListPopupWindow"); + } + + @Test + public void testShortGoodNamesVsGeneratedAliases() { + assertThatBetterClassName("Room", "C2937kh").isEqualTo("Room"); + assertThatBetterClassName("Fade", "C1428qi").isEqualTo("Fade"); + assertThatBetterClassName("Scene", "C4063yi").isEqualTo("Scene"); + } + + @Test + public void testGoodNamesVsGeneratedAliasesWithPrefix() { + assertThatBetterClassName("AppCompatActivity", "ActivityC2646h0").isEqualTo("AppCompatActivity"); + assertThatBetterClassName("PagerAdapter", "AbstractC3038lk").isEqualTo("PagerAdapter"); + assertThatBetterClassName("Lazy", "InterfaceC6434a").isEqualTo("Lazy"); + assertThatBetterClassName("MembersInjector", "InterfaceC6435b").isEqualTo("MembersInjector"); + assertThatBetterClassName("Subscriber", "InterfaceC6439c").isEqualTo("Subscriber"); + } + + @Test + public void testGoodNamesWithDigitsVsGeneratedAliases() { + assertThatBetterClassName("ISO8061Formatter", "C1121uq4").isEqualTo("ISO8061Formatter"); + assertThatBetterClassName("Jdk9Platform", "C1189rn6").isEqualTo("Jdk9Platform"); + assertThatBetterClassName("WrappedDrawableApi14", "C2847i9").isEqualTo("WrappedDrawableApi14"); + assertThatBetterClassName("WrappedDrawableApi21", "C2888j9").isEqualTo("WrappedDrawableApi21"); + } + + @Test + public void testShortNamesVsLongNames() { + assertThatBetterClassName("az", "Observer").isEqualTo("Observer"); + assertThatBetterClassName("bb", "RenderEvent").isEqualTo("RenderEvent"); + assertThatBetterClassName("aaaa", "FontUtils").isEqualTo("FontUtils"); + } + + /** + * Tests {@link BetterName#getBetterClassName(String, String)} on equally good names. + * In this case, according to the documentation, the method should return the first argument. + * + * @see BetterName#getBetterClassName(String, String) + */ + @Test + public void testEquallyGoodNames() { + assertThatBetterClassName("AAAA", "BBBB").isEqualTo("AAAA"); + assertThatBetterClassName("BBBB", "AAAA").isEqualTo("BBBB"); + + assertThatBetterClassName("XYXYXY", "YZYZYZ").isEqualTo("XYXYXY"); + assertThatBetterClassName("YZYZYZ", "XYXYXY").isEqualTo("YZYZYZ"); + } + + private AbstractStringAssert assertThatBetterClassName(String firstName, String secondName) { + return assertThat(getBetterClassName(firstName, secondName)); + } +} diff --git a/jadx-core/src/test/java/jadx/core/utils/TestGetBetterResourceName.java b/jadx-core/src/test/java/jadx/core/utils/TestGetBetterResourceName.java new file mode 100644 index 000000000..74b487b75 --- /dev/null +++ b/jadx-core/src/test/java/jadx/core/utils/TestGetBetterResourceName.java @@ -0,0 +1,37 @@ +package jadx.core.utils; + +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.Test; + +import static jadx.core.utils.BetterName.getBetterResourceName; +import static org.assertj.core.api.Assertions.assertThat; + +public class TestGetBetterResourceName { + + @Test + public void testGoodNamesVsSyntheticNames() { + assertThatBetterResourceName("color_main", "t0").isEqualTo("color_main"); + assertThatBetterResourceName("done", "oOo0oO0o").isEqualTo("done"); + } + + /** + * Tests {@link BetterName#getBetterResourceName(String, String)} on equally good names. + * In this case, according to the documentation, the method should return the first argument. + * + * @see BetterName#getBetterResourceName(String, String) + */ + @Test + public void testEquallyGoodNames() { + assertThatBetterResourceName("AAAA", "BBBB").isEqualTo("AAAA"); + assertThatBetterResourceName("BBBB", "AAAA").isEqualTo("BBBB"); + + assertThatBetterResourceName("Theme.AppCompat.Light", "Theme_AppCompat_Light") + .isEqualTo("Theme.AppCompat.Light"); + assertThatBetterResourceName("Theme_AppCompat_Light", "Theme.AppCompat.Light") + .isEqualTo("Theme_AppCompat_Light"); + } + + private AbstractStringAssert assertThatBetterResourceName(String firstName, String secondName) { + return assertThat(getBetterResourceName(firstName, secondName)); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/rename/TestUsingSourceFileName.java b/jadx-core/src/test/java/jadx/tests/integration/rename/TestUsingSourceFileName.java index faa3b4556..6f1d9ac17 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/rename/TestUsingSourceFileName.java +++ b/jadx-core/src/test/java/jadx/tests/integration/rename/TestUsingSourceFileName.java @@ -17,6 +17,14 @@ public class TestUsingSourceFileName extends SmaliTest { .containsOne("class b {"); } + @Test + public void testIfBetterUseSourceName() { + args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.IF_BETTER); + assertThat(searchCls(loadFromSmaliFiles(), "b")) + .code() + .containsOne("class a {"); + } + @Test public void testAlwaysUseSourceName() { args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.ALWAYS); @@ -36,6 +44,17 @@ public class TestUsingSourceFileName extends SmaliTest { .containsOne("/* compiled from: a.java */"); } + @Test + public void testIfBetterUseSourceNameWithDeobf() { + args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.IF_BETTER); + enableDeobfuscation(); + args.setDeobfuscationMinLength(100); // rename everything + assertThat(searchCls(loadFromSmaliFiles(), "b")) + .code() + .containsOne("class a {") + .containsOne("/* compiled from: a.java */"); + } + @Test public void testAlwaysUseSourceNameWithDeobf() { args.setUseSourceNameAsClassNameAlias(UseSourceNameAsClassNameAlias.ALWAYS); @@ -66,9 +85,9 @@ public class TestUsingSourceFileName extends SmaliTest { } @Test - public void testDeprecatedUseSourceNameWithDeobf() { + public void testDeprecatedDontUseSourceNameWithDeobf() { // noinspection deprecation - args.setUseSourceNameAsClassAlias(true); + args.setUseSourceNameAsClassAlias(false); enableDeobfuscation(); args.setDeobfuscationMinLength(100); // rename everything assertThat(searchCls(loadFromSmaliFiles(), "b")) @@ -76,4 +95,16 @@ public class TestUsingSourceFileName extends SmaliTest { .containsOne("class C0000b {") .containsOne("/* compiled from: a.java */"); } + + @Test + public void testDeprecatedUseSourceNameWithDeobf() { + // noinspection deprecation + args.setUseSourceNameAsClassAlias(true); + enableDeobfuscation(); + args.setDeobfuscationMinLength(100); // rename everything + assertThat(searchCls(loadFromSmaliFiles(), "b")) + .code() + .containsOne("class a {") + .containsOne("/* compiled from: a.java */"); + } }