diff --git a/jadx-core/src/main/java/jadx/core/codegen/TypeGen.java b/jadx-core/src/main/java/jadx/core/codegen/TypeGen.java index 2fc6845d4..a7c76e65c 100644 --- a/jadx-core/src/main/java/jadx/core/codegen/TypeGen.java +++ b/jadx-core/src/main/java/jadx/core/codegen/TypeGen.java @@ -1,5 +1,6 @@ package jadx.core.codegen; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -97,6 +98,43 @@ public class TypeGen { } } + @Nullable + public static String literalToRawString(LiteralArg arg) { + ArgType type = arg.getType(); + if (type == null) { + return null; + } + long lit = arg.getLiteral(); + switch (type.getPrimitiveType()) { + case BOOLEAN: + return lit == 0 ? "false" : "true"; + case CHAR: + return String.valueOf((char) lit); + + case BYTE: + case SHORT: + case INT: + case LONG: + return Long.toString(lit); + + case FLOAT: + return Float.toString(Float.intBitsToFloat((int) lit)); + case DOUBLE: + return Double.toString(Double.longBitsToDouble(lit)); + + case OBJECT: + case ARRAY: + if (lit != 0) { + LOG.warn("Wrong object literal: {} for type: {}", lit, type); + return Long.toString(lit); + } + return "null"; + + default: + return null; + } + } + public static String formatShort(long l, boolean cast) { if (l == Short.MAX_VALUE) { return "Short.MAX_VALUE"; diff --git a/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java b/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java index ee7dff9e5..a5f5c5a19 100644 --- a/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java +++ b/jadx-core/src/main/java/jadx/core/dex/instructions/args/InsnArg.java @@ -232,6 +232,10 @@ public abstract class InsnArg extends Typed { return contains(AFlag.THIS); } + public boolean isConst() { + return isLiteral() || (isInsnWrap() && ((InsnWrapArg) this).getWrapInsn().isConstInsn()); + } + protected final T copyCommonParams(T copy) { copy.copyAttributesFrom(this); copy.setParentInsn(parentInsn); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java index 8a1b18373..e263429c0 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/SimplifyVisitor.java @@ -4,10 +4,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.core.Consts; +import jadx.core.codegen.TypeGen; import jadx.core.deobf.NameMapper; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.info.ClassInfo; @@ -386,7 +388,7 @@ public class SimplifyVisitor extends AbstractVisitor { } } if (!stringArgFound) { - // TODO: convert one arg to string using `String.valueOf()` + mth.addDebugComment("TODO: convert one arg to string using `String.valueOf()`, args: " + args); return null; } @@ -394,7 +396,8 @@ public class SimplifyVisitor extends AbstractVisitor { removeStringBuilderInsns(mth, toStrInsn, chain); List dupArgs = Utils.collectionMap(args, InsnArg::duplicate); - InsnNode concatInsn = new InsnNode(InsnType.STR_CONCAT, dupArgs); + List simplifiedArgs = concatConstArgs(dupArgs); + InsnNode concatInsn = new InsnNode(InsnType.STR_CONCAT, simplifiedArgs); concatInsn.setResult(toStrInsn.getResult()); concatInsn.add(AFlag.SYNTHETIC); concatInsn.copyAttributesFrom(toStrInsn); @@ -408,6 +411,67 @@ public class SimplifyVisitor extends AbstractVisitor { return null; } + private static boolean isConstConcatNeeded(List args) { + boolean prevConst = false; + for (InsnArg arg : args) { + boolean curConst = arg.isConst(); + if (curConst && prevConst) { + // found 2 consecutive constants + return true; + } + prevConst = curConst; + } + return false; + } + + private static List concatConstArgs(List args) { + if (!isConstConcatNeeded(args)) { + return args; + } + int size = args.size(); + List newArgs = new ArrayList<>(size); + List concatList = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + InsnArg arg = args.get(i); + String constStr = getConstString(arg); + if (constStr != null) { + concatList.add(constStr); + } else { + if (!concatList.isEmpty()) { + newArgs.add(getConcatArg(concatList, args, i)); + concatList.clear(); + } + newArgs.add(arg); + } + } + if (!concatList.isEmpty()) { + newArgs.add(getConcatArg(concatList, args, size)); + } + return newArgs; + } + + private static InsnArg getConcatArg(List concatList, List args, int idx) { + if (concatList.size() == 1) { + return args.get(idx - 1); + } + String str = Utils.concatStrings(concatList); + return InsnArg.wrapArg(new ConstStringNode(str)); + } + + @Nullable + private static String getConstString(InsnArg arg) { + if (arg.isLiteral()) { + return TypeGen.literalToRawString((LiteralArg) arg); + } + if (arg.isInsnWrap()) { + InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn(); + if (wrapInsn instanceof ConstStringNode) { + return ((ConstStringNode) wrapInsn).getString(); + } + } + return null; + } + /* String concat without assign to variable will cause compilation error */ private static void checkResult(MethodNode mth, InsnNode concatInsn) { if (concatInsn.getResult() == null) { diff --git a/jadx-core/src/main/java/jadx/core/utils/Utils.java b/jadx-core/src/main/java/jadx/core/utils/Utils.java index 219701318..7ff1ed321 100644 --- a/jadx-core/src/main/java/jadx/core/utils/Utils.java +++ b/jadx-core/src/main/java/jadx/core/utils/Utils.java @@ -101,6 +101,18 @@ public class Utils { return sb.toString(); } + public static String concatStrings(List list) { + if (isEmpty(list)) { + return ""; + } + if (list.size() == 1) { + return list.get(0); + } + StringBuilder sb = new StringBuilder(); + list.forEach(sb::append); + return sb.toString(); + } + public static String getStackTrace(Throwable throwable) { if (throwable == null) { return ""; diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestConstStringConcat.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestConstStringConcat.java new file mode 100644 index 000000000..8d02ee8de --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestConstStringConcat.java @@ -0,0 +1,41 @@ +package jadx.tests.integration.others; + +import org.junit.jupiter.api.Test; + +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestConstStringConcat extends IntegrationTest { + + @SuppressWarnings("StringBufferReplaceableByString") + public static class TestCls { + public String test1(int value) { + return new StringBuilder().append("Value").append(" equals ").append(value).toString(); + } + + public String test2() { + return new StringBuilder().append("App ").append("version: ").append(1).append('.').append(2).toString(); + } + + public String test3(String name, int value) { + return "value " + name + " = " + value; + } + + public void check() { + assertThat(test1(7)).isEqualTo("Value equals 7"); + assertThat(test2()).isEqualTo("App version: 1.2"); + assertThat(test3("v", 4)).isEqualTo("value v = 4"); + } + } + + @Test + public void test() { + noDebugInfo(); + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("return \"Value equals \" + ") + .containsOne("return \"App version: 1.2\";") + .containsOne("return \"value \" + str + \" = \" + i;"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination2.java b/jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination2.java index bab664d8a..96d8e49ab 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/others/TestStringBuilderElimination2.java @@ -14,6 +14,7 @@ import static org.hamcrest.MatcherAssert.assertThat; * * @author Jan Peter Stotz */ +@SuppressWarnings("StringBufferReplaceableByString") public class TestStringBuilderElimination2 extends IntegrationTest { public static class TestCls1 { @@ -27,7 +28,7 @@ public class TestStringBuilderElimination2 extends IntegrationTest { public void test1() { ClassNode cls = getClassNode(TestStringBuilderElimination2.TestCls1.class); String code = cls.getCode().toString(); - assertThat(code, containsString("return \"[init]\" + \"a1\" + 'c' + 2 + 0L + 1.0f + 2.0d + true;")); + assertThat(code, containsString("return \"[init]a1c201.02.0true\";")); } public static class TestCls2 { @@ -49,7 +50,7 @@ public class TestStringBuilderElimination2 extends IntegrationTest { public void test2() { ClassNode cls = getClassNode(TestStringBuilderElimination2.TestCls2.class); String code = cls.getCode().toString(); - assertThat(code, containsString("return \"[init]\" + \"a1\" + 'c' + 1 + 2L + 1.0f + 2.0d + true;")); + assertThat(code, containsString("return \"[init]a1c121.02.0true\";")); } public static class TestClsStringUtilsReverse {