From cb9693a9d1219a335b22110eee5f97d5cc57cdc4 Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Thu, 19 Jun 2025 21:55:23 +0100 Subject: [PATCH] fix: correct hex format for negative numbers (#2531) --- .../java/jadx/core/utils/StringUtils.java | 103 ++++++++++++------ .../integration/arith/TestNumbersFormat.java | 57 ++++++++++ .../integration/arith/TestSpecialValues.java | 10 +- .../integration/arith/TestSpecialValues2.java | 2 - .../integration/arrays/TestRedundantType.java | 71 ------------ .../integration/conditions/TestCast.java | 4 +- .../integration/types/TestTypeResolver9.java | 7 +- 7 files changed, 134 insertions(+), 120 deletions(-) create mode 100644 jadx-core/src/test/java/jadx/tests/integration/arith/TestNumbersFormat.java delete mode 100644 jadx-core/src/test/java/jadx/tests/integration/arrays/TestRedundantType.java diff --git a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java index e3d9179f2..c45bb88ea 100644 --- a/jadx-core/src/main/java/jadx/core/utils/StringUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/StringUtils.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.Nullable; import jadx.api.JadxArgs; import jadx.api.args.IntegerFormat; import jadx.core.deobf.NameMapper; +import jadx.core.utils.exceptions.JadxRuntimeException; public class StringUtils { private static final StringUtils DEFAULT_INSTANCE = new StringUtils(new JadxArgs()); @@ -386,58 +387,90 @@ public class StringUtils { return new SimpleDateFormat("HH:mm:ss").format(new Date()); } - private String toFormatString(long l) { + private String formatNumber(long number, int bytesLen, boolean cast) { + String numStr; if (integerFormat.isHexadecimal()) { - return "0x" + Long.toHexString(l); + String hexStr = Long.toHexString(number); + if (number < 0) { + // cut leading 'f' for negative numbers to match number type length + int len = hexStr.length(); + numStr = "0x" + hexStr.substring(len - bytesLen * 2, len); + // force cast, because unsigned negative numbers are bigger + // than signed max value allowed by compiler + cast = true; + } else { + numStr = "0x" + hexStr; + } + } else { + numStr = Long.toString(number); } - return Long.toString(l); + if (bytesLen == 8 && (number == Long.MIN_VALUE || Math.abs(number) >= Integer.MAX_VALUE)) { + // force cast for long values bigger than min/max int + // to resolve compiler error: "integer number too large" + cast = true; + } + if (cast) { + if (bytesLen == 8) { + return numStr + 'L'; + } + return getCastStr(bytesLen) + numStr; + } + return numStr; } - public String formatShort(long l, boolean cast) { - if (l == Short.MAX_VALUE) { - return "Short.MAX_VALUE"; + private static String getCastStr(int bytesLen) { + switch (bytesLen) { + case 1: + return "(byte) "; + case 2: + return "(short) "; + case 4: + return "(int) "; + case 8: + return "(long) "; + default: + throw new JadxRuntimeException("Unexpected number type length: " + bytesLen); } - if (l == Short.MIN_VALUE) { - return "Short.MIN_VALUE"; - } - String str = toFormatString(l); - return cast ? "(short) " + str : str; } public String formatByte(long l, boolean cast) { - if (l == Byte.MAX_VALUE) { - return "Byte.MAX_VALUE"; + return formatNumber(l, 1, cast); + } + + public String formatShort(long l, boolean cast) { + if (integerFormat == IntegerFormat.AUTO) { + switch ((short) l) { + case Short.MAX_VALUE: + return "Short.MAX_VALUE"; + case Short.MIN_VALUE: + return "Short.MIN_VALUE"; + } } - if (l == Byte.MIN_VALUE) { - return "Byte.MIN_VALUE"; - } - String str = toFormatString(l); - return cast ? "(byte) " + str : str; + return formatNumber(l, 2, cast); } public String formatInteger(long l, boolean cast) { - if (l == Integer.MAX_VALUE) { - return "Integer.MAX_VALUE"; + if (integerFormat == IntegerFormat.AUTO) { + switch ((int) l) { + case Integer.MAX_VALUE: + return "Integer.MAX_VALUE"; + case Integer.MIN_VALUE: + return "Integer.MIN_VALUE"; + } } - if (l == Integer.MIN_VALUE) { - return "Integer.MIN_VALUE"; - } - String str = toFormatString(l); - return cast ? "(int) " + str : str; + return formatNumber(l, 4, cast); } public String formatLong(long l, boolean cast) { - if (l == Long.MAX_VALUE) { - return "Long.MAX_VALUE"; + if (integerFormat == IntegerFormat.AUTO) { + if (l == Long.MAX_VALUE) { + return "Long.MAX_VALUE"; + } + if (l == Long.MIN_VALUE) { + return "Long.MIN_VALUE"; + } } - if (l == Long.MIN_VALUE) { - return "Long.MIN_VALUE"; - } - String str = toFormatString(l); - if (cast || Math.abs(l) >= Integer.MAX_VALUE) { - return str + 'L'; - } - return str; + return formatNumber(l, 8, cast); } public static String formatDouble(double d) { diff --git a/jadx-core/src/test/java/jadx/tests/integration/arith/TestNumbersFormat.java b/jadx-core/src/test/java/jadx/tests/integration/arith/TestNumbersFormat.java new file mode 100644 index 000000000..3b2a8a6c0 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/integration/arith/TestNumbersFormat.java @@ -0,0 +1,57 @@ +package jadx.tests.integration.arith; + +import org.junit.jupiter.api.Test; + +import jadx.api.args.IntegerFormat; +import jadx.tests.api.IntegrationTest; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; + +public class TestNumbersFormat extends IntegrationTest { + + @SuppressWarnings({ "FieldCanBeLocal", "UnusedAssignment", "unused" }) + public static class TestCls { + private Object obj; + + public void test() { + obj = new byte[] { 0, -1, -0xA, (byte) 0xff, Byte.MIN_VALUE, Byte.MAX_VALUE }; + obj = new short[] { 0, -1, -0xA, (short) 0xffff, Short.MIN_VALUE, Short.MAX_VALUE }; + obj = new int[] { 0, -1, -0xA, 0xffff_ffff, Integer.MIN_VALUE, Integer.MAX_VALUE }; + obj = new long[] { 0, -1, -0xA, 0xffff_ffff_ffff_ffffL, Long.MIN_VALUE, Long.MAX_VALUE }; + } + } + + @Test + public void test() { + getArgs().setIntegerFormat(IntegerFormat.AUTO); + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("new byte[]{0, -1, -10, -1, -128, 127}") + .containsOne("new short[]{0, -1, -10, -1, Short.MIN_VALUE, Short.MAX_VALUE}") + .containsOne("new int[]{0, -1, -10, -1, Integer.MIN_VALUE, Integer.MAX_VALUE}") + .containsOne("new long[]{0, -1, -10, -1, Long.MIN_VALUE, Long.MAX_VALUE}"); + } + + @Test + public void testDecimalFormat() { + getArgs().setIntegerFormat(IntegerFormat.DECIMAL); + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("new byte[]{0, -1, -10, -1, -128, 127}") + .containsOne("new short[]{0, -1, -10, -1, -32768, 32767}") + .containsOne("new int[]{0, -1, -10, -1, -2147483648, 2147483647}") + .containsOne("new long[]{0, -1, -10, -1, -9223372036854775808L, 9223372036854775807L}"); + } + + @Test + public void testHexFormat() { + getArgs().setIntegerFormat(IntegerFormat.HEXADECIMAL); + assertThat(getClassNode(TestCls.class)) + .code() + .containsOne("new byte[]{0x0, (byte) 0xff, (byte) 0xf6, (byte) 0xff, (byte) 0x80, 0x7f}") + .containsOne("new short[]{0x0, (short) 0xffff, (short) 0xfff6, (short) 0xffff, (short) 0x8000, 0x7fff}") + .containsOne("new int[]{0x0, (int) 0xffffffff, (int) 0xfffffff6, (int) 0xffffffff, (int) 0x80000000, 0x7fffffff}") + .containsOne( + "new long[]{0x0, 0xffffffffffffffffL, 0xfffffffffffffff6L, 0xffffffffffffffffL, 0x8000000000000000L, 0x7fffffffffffffffL}"); + } +} diff --git a/jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues.java b/jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues.java index ced7a66d9..3dd7e161c 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues.java +++ b/jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues.java @@ -3,7 +3,8 @@ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; -import jadx.tests.api.utils.assertj.JadxAssertions; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestSpecialValues extends IntegrationTest { @@ -11,7 +12,6 @@ public class TestSpecialValues extends IntegrationTest { public void test() { shorts(Short.MIN_VALUE, Short.MAX_VALUE); - bytes(Byte.MIN_VALUE, Byte.MAX_VALUE); ints(Integer.MIN_VALUE, Integer.MAX_VALUE); longs(Long.MIN_VALUE, Long.MAX_VALUE); @@ -25,9 +25,6 @@ public class TestSpecialValues extends IntegrationTest { private void shorts(short... v) { } - private void bytes(byte... v) { - } - private void ints(int... v) { } @@ -43,14 +40,13 @@ public class TestSpecialValues extends IntegrationTest { @Test public void test() { - JadxAssertions.assertThat(getClassNode(TestCls.class)) + assertThat(getClassNode(TestCls.class)) .code() .containsOne( "Float.NaN, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY, Float.MIN_VALUE, Float.MAX_VALUE, Float.MIN_NORMAL") .containsOne("Double.NaN, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, " + "Double.MIN_VALUE, Double.MAX_VALUE, Double.MIN_NORMAL") .containsOne("Short.MIN_VALUE, Short.MAX_VALUE") - .containsOne("Byte.MIN_VALUE, Byte.MAX_VALUE") .containsOne("Integer.MIN_VALUE, Integer.MAX_VALUE") .containsOne("Long.MIN_VALUE, Long.MAX_VALUE"); } diff --git a/jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues2.java b/jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues2.java index fb320ab7a..6cf5b47c1 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues2.java +++ b/jadx-core/src/test/java/jadx/tests/integration/arith/TestSpecialValues2.java @@ -2,7 +2,6 @@ package jadx.tests.integration.arith; import org.junit.jupiter.api.Test; -import jadx.NotYetImplemented; import jadx.tests.api.IntegrationTest; import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; @@ -15,7 +14,6 @@ public class TestSpecialValues2 extends IntegrationTest { } } - @NotYetImplemented("Constant value replace") @Test public void test() { noDebugInfo(); diff --git a/jadx-core/src/test/java/jadx/tests/integration/arrays/TestRedundantType.java b/jadx-core/src/test/java/jadx/tests/integration/arrays/TestRedundantType.java deleted file mode 100644 index d1d5c067d..000000000 --- a/jadx-core/src/test/java/jadx/tests/integration/arrays/TestRedundantType.java +++ /dev/null @@ -1,71 +0,0 @@ -package jadx.tests.integration.arrays; - -import org.junit.jupiter.api.Test; - -import jadx.tests.api.IntegrationTest; -import jadx.tests.api.utils.assertj.JadxAssertions; - -import static org.assertj.core.api.Assertions.assertThat; - -public class TestRedundantType extends IntegrationTest { - - public static class TestCls { - - public byte[] method() { - return new byte[] { 10, 11, 12 }; - } - } - - @Test - public void test() { - JadxAssertions.assertThat(getClassNode(TestCls.class)) - .code() - .contains("return new byte[]{10, 11, 12};"); - } - - public static class TestByte { - - public byte[] method() { - byte[] arr = new byte[50]; - arr[10] = 126; - arr[20] = 127; - arr[30] = (byte) 128; - arr[40] = (byte) 129; - return arr; - } - } - - @Test - public void testByte() { - JadxAssertions.assertThat(getClassNode(TestByte.class)) - .code() - .contains("arr[10] = 126") - .contains("arr[20] = Byte.MAX_VALUE") - .contains("arr[30] = Byte.MIN_VALUE") - .contains("arr[40] = -127"); - assertThat(new TestByte().method()[40]).isEqualTo((byte) -127); - } - - public static class TestShort { - - public short[] method() { - short[] arr = new short[50]; - arr[10] = 32766; - arr[20] = 32767; - arr[30] = (short) 32768; - arr[40] = (short) 32769; - return arr; - } - } - - @Test - public void testShort() { - JadxAssertions.assertThat(getClassNode(TestShort.class)) - .code() - .contains("arr[10] = 32766") - .contains("arr[20] = Short.MAX_VALUE") - .contains("arr[30] = Short.MIN_VALUE") - .contains("arr[40] = -32767"); - assertThat(new TestShort().method()[40]).isEqualTo((short) -32767); - } -} diff --git a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestCast.java b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestCast.java index 8144eacf0..f9489da95 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/conditions/TestCast.java +++ b/jadx-core/src/test/java/jadx/tests/integration/conditions/TestCast.java @@ -21,7 +21,7 @@ public class TestCast extends IntegrationTest { } public void test3(boolean a) { - write(a ? 0 : Byte.MAX_VALUE); + write(a ? 0 : (byte) 127); } public void test4(boolean a) { @@ -49,7 +49,7 @@ public class TestCast extends IntegrationTest { .code() .contains("write(a ? (byte) 0 : (byte) 1);") .contains("write(a ? (byte) 0 : this.myByte);") - .contains("write(a ? (byte) 0 : Byte.MAX_VALUE);") + .contains("write(a ? (byte) 0 : (byte) 127);") .contains("write(a ? (short) 0 : (short) 1);") .contains("write(a ? this.myShort : (short) 0);") .contains("write(a ? Short.MIN_VALUE : (short) 0);"); diff --git a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver9.java b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver9.java index 00bdac8ae..8120534ea 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver9.java +++ b/jadx-core/src/test/java/jadx/tests/integration/types/TestTypeResolver9.java @@ -3,7 +3,8 @@ package jadx.tests.integration.types; import org.junit.jupiter.api.Test; import jadx.tests.api.IntegrationTest; -import jadx.tests.api.utils.assertj.JadxAssertions; + +import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat; public class TestTypeResolver9 extends IntegrationTest { @@ -13,13 +14,13 @@ public class TestTypeResolver9 extends IntegrationTest { } public int test2(byte[] array, int offset) { - return (array[offset] * 128) + (array[offset + 1] & 0xFF); + return array[offset] * 128 + (array[offset + 1] & 0xFF); } } @Test public void test() { - JadxAssertions.assertThat(getClassNode(TestCls.class)) + assertThat(getClassNode(TestCls.class)) .code() .containsOne("return 16777216 * b;") .doesNotContain("Byte.MIN_VALUE");