diff --git a/src/main/java/daomephsta/unpick/impl/AbstractInsnNodes.java b/src/main/java/daomephsta/unpick/impl/AbstractInsnNodes.java index f802efd..004a5b7 100644 --- a/src/main/java/daomephsta/unpick/impl/AbstractInsnNodes.java +++ b/src/main/java/daomephsta/unpick/impl/AbstractInsnNodes.java @@ -51,6 +51,32 @@ public static Object getLiteralValue(AbstractInsnNode insn) public static boolean isLiteral(AbstractInsnNode insn, Object literal) { - return getLiteralValue(insn).equals(literal); + Object literalValue = getLiteralValue(insn); + // Chars are stored as ints, so conversion is necessary + if (literal instanceof Character && literalValue instanceof Integer) + { + Character charLiteral = (char) (int) literalValue; + return literal.equals(charLiteral); + } + // Compare integers by long value, to support widening comparisons + if (isIntegral(literalValue) && isIntegral(literal)) + return ((Number) literalValue).longValue() == ((Number) literal).longValue(); + // Compare floating point numbers by double value, to support widening comparisons + if (isFloatingPoint(literalValue) && isFloatingPoint(literal)) + return ((Number) literalValue).doubleValue() == ((Number) literal).doubleValue(); + return literalValue.equals(literal); + } + + private static boolean isIntegral(Object literal) + { + return literal instanceof Byte || + literal instanceof Short || + literal instanceof Integer || + literal instanceof Long; + } + + private static boolean isFloatingPoint(Object literal) + { + return literal instanceof Float || literal instanceof Double; } } diff --git a/src/main/java/daomephsta/unpick/impl/InstructionFactory.java b/src/main/java/daomephsta/unpick/impl/InstructionFactory.java index 5d654d8..d820ce7 100644 --- a/src/main/java/daomephsta/unpick/impl/InstructionFactory.java +++ b/src/main/java/daomephsta/unpick/impl/InstructionFactory.java @@ -19,9 +19,11 @@ else if (number instanceof Double) return pushesDouble(number.doubleValue()); else if (number instanceof Float) return pushesFloat(number.floatValue()); - else //Shorts, bytes, and chars are all ints internally + else //Shorts and bytes are all ints internally return pushesInt(number.intValue()); } + else if (value instanceof Character) + return pushesChar((char) value); else if (value instanceof Boolean) return pushesBoolean((boolean) value); else if (value instanceof String) @@ -43,9 +45,11 @@ else if (number instanceof Double) pushesDouble(method, number.doubleValue()); else if (number instanceof Float) pushesFloat(method, number.floatValue()); - else //Shorts, bytes, and chars are all ints internally + else //Shorts and bytes are all ints internally pushesInt(method, number.intValue()); } + else if (value instanceof Character) + pushesChar(method, (char) value); else if (value instanceof Boolean) pushesBoolean(method, (boolean) value); else if (value instanceof String) @@ -66,6 +70,16 @@ public static void pushesBoolean(MethodVisitor method, boolean bool) method.visitInsn(bool ? ICONST_1 : ICONST_0); } + public static AbstractInsnNode pushesChar(char c) + { + return pushesInt(c); + } + + public static void pushesChar(MethodVisitor method, char c) + { + pushesInt(method, c); + } + private static final int[] I_OPCODES = {ICONST_0, ICONST_1, ICONST_2, ICONST_3, ICONST_4, ICONST_5}; public static AbstractInsnNode pushesInt(int i) { diff --git a/src/main/java/daomephsta/unpick/impl/IntegerType.java b/src/main/java/daomephsta/unpick/impl/IntegerType.java index 2158789..ff66d1e 100644 --- a/src/main/java/daomephsta/unpick/impl/IntegerType.java +++ b/src/main/java/daomephsta/unpick/impl/IntegerType.java @@ -1,11 +1,67 @@ package daomephsta.unpick.impl; +import java.util.Arrays; +import java.util.Locale; +import java.util.stream.Collectors; + import org.objectweb.asm.*; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnNode; public enum IntegerType { + BYTE(Byte.class, byte.class, Type.BYTE_TYPE, Opcodes.IAND, Opcodes.IRETURN) + { + @Override + public AbstractInsnNode createLiteralPushInsn(long literal) + { return InstructionFactory.pushesInt((byte) literal); } + + @Override + public void appendLiteralPushInsn(MethodVisitor mv, long literal) + { InstructionFactory.pushesInt(mv, (byte) literal); } + + @Override + public Number box(long value) + { return Byte.valueOf((byte) value); } + + @Override + public Number binaryNegate(Number value) + { return (byte) ~value.byteValue(); } + + @Override + public long toUnsignedLong(Number value) + { return Byte.toUnsignedLong(value.byteValue()); } + + @Override + public Number parse(String valueString) + { return Byte.parseByte(valueString); } + }, + SHORT(Short.class, short.class, Type.SHORT_TYPE, Opcodes.IAND, Opcodes.IRETURN) + { + @Override + public AbstractInsnNode createLiteralPushInsn(long literal) + { return InstructionFactory.pushesInt((short) literal); } + + @Override + public void appendLiteralPushInsn(MethodVisitor mv, long literal) + { InstructionFactory.pushesInt(mv, (short) literal); } + + @Override + public Number box(long value) + { return Short.valueOf((short) value); } + + @Override + public Number binaryNegate(Number value) + { return (short) ~value.shortValue(); } + + @Override + public long toUnsignedLong(Number value) + { return Short.toUnsignedLong(value.shortValue()); } + + @Override + public Number parse(String valueString) + { return Short.parseShort(valueString); } + }, INT(Integer.class, int.class, Type.INT_TYPE, Opcodes.IAND, Opcodes.IRETURN) { @Override @@ -18,7 +74,7 @@ public void appendLiteralPushInsn(MethodVisitor mv, long literal) @Override public Number box(long value) - { return new Integer((int) value); } + { return Integer.valueOf((int) value); } @Override public Number binaryNegate(Number value) @@ -27,6 +83,10 @@ public Number binaryNegate(Number value) @Override public long toUnsignedLong(Number value) { return Integer.toUnsignedLong(value.intValue()); } + + @Override + public Number parse(String valueString) + { return Integer.parseInt(valueString); } }, LONG(Long.class, long.class, Type.LONG_TYPE, Opcodes.LAND, Opcodes.LRETURN) { @@ -40,7 +100,7 @@ public void appendLiteralPushInsn(MethodVisitor mv, long literal) @Override public Number box(long value) - { return new Long(value); } + { return Long.valueOf(value); } @Override public Number binaryNegate(Number value) @@ -49,6 +109,10 @@ public Number binaryNegate(Number value) @Override public long toUnsignedLong(Number value) { return value.longValue(); } + + @Override + public Number parse(String valueString) + { return Long.parseLong(valueString); } }; private final Class boxed, primitive; @@ -64,14 +128,31 @@ private IntegerType(Class boxed, Class primi this.returnOpcode = returnOpcode; } - public static IntegerType from(Class clazz) + public static IntegerType from(Type type) + { + for (IntegerType intType : values()) + { + if (intType.type == type) + return intType; + } + throw new IllegalArgumentException(type + " is not one of: " + describeValidTypes()); + } + + public static IntegerType from(Object literal) + { + for (IntegerType type : values()) + { + if (literal.getClass() == type.getBoxClass() || literal.getClass() == type.getPrimitiveClass()) + return type; + } + throw new IllegalArgumentException(literal + " is not one of: " + describeValidTypes()); + } + + private static String describeValidTypes() { - if (clazz == Integer.class || clazz == int.class) - return INT; - else if (clazz == Long.class || clazz == long.class) - return LONG; - else - throw new IllegalArgumentException("Expected an integer or long, got " + clazz); + return Arrays.stream(values()) + .map(t -> t.name().toLowerCase(Locale.ROOT)) + .collect(Collectors.joining(", ")); } public AbstractInsnNode createAndInsn() @@ -158,4 +239,6 @@ public Class getPrimitiveClass() public abstract Number binaryNegate(Number value); public abstract long toUnsignedLong(Number value); + + public abstract Number parse(String valueString); } diff --git a/src/main/java/daomephsta/unpick/impl/LiteralType.java b/src/main/java/daomephsta/unpick/impl/LiteralType.java index c79a912..940bcd1 100644 --- a/src/main/java/daomephsta/unpick/impl/LiteralType.java +++ b/src/main/java/daomephsta/unpick/impl/LiteralType.java @@ -1,7 +1,12 @@ package daomephsta.unpick.impl; +import java.util.Arrays; import java.util.HashMap; +import java.util.Locale; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.objectweb.asm.*; import org.objectweb.asm.tree.AbstractInsnNode; @@ -9,6 +14,57 @@ public enum LiteralType { + BYTE(Byte.class, byte.class, Type.BYTE_TYPE, Opcodes.IRETURN) + { + @Override + public AbstractInsnNode createLiteralPushInsn(Object literal) + { return InstructionFactory.pushesInt(((Number) literal).byteValue()); } + + @Override + public void appendLiteralPushInsn(MethodVisitor mv, Object literal) + { InstructionFactory.pushesInt(mv, ((Number) literal).byteValue()); } + + @Override + public Object parse(String valueString) + { return Byte.parseByte(valueString); } + }, + SHORT(Short.class, short.class, Type.SHORT_TYPE, Opcodes.IRETURN) + { + @Override + public AbstractInsnNode createLiteralPushInsn(Object literal) + { return InstructionFactory.pushesInt(((Number) literal).shortValue()); } + + @Override + public void appendLiteralPushInsn(MethodVisitor mv, Object literal) + { InstructionFactory.pushesInt(mv, ((Number) literal).shortValue()); } + + @Override + public Object parse(String valueString) + { return Short.parseShort(valueString); } + }, + CHAR(Character.class, char.class, Type.CHAR_TYPE, Opcodes.IRETURN) + { + @Override + public AbstractInsnNode createLiteralPushInsn(Object literal) + { return InstructionFactory.pushesChar((char) literal); } + + @Override + public void appendLiteralPushInsn(MethodVisitor mv, Object literal) + { InstructionFactory.pushesChar(mv, (char) literal); } + + @Override + public Object parse(String valueString) + { + // Unicode escape parsing + Matcher m = UNICODE_ESCAPE.matcher(valueString); + if (m.matches()) + return (char) Integer.parseInt(m.group(1), 16); + // Plain java char parsing + if (valueString.length() != 1) + throw new IllegalArgumentException(valueString + " is not a single character or valid unicode escape"); + return valueString.charAt(0); + } + }, INT(Integer.class, int.class, Type.INT_TYPE, Opcodes.IRETURN) { @Override @@ -94,6 +150,7 @@ public Object parse(String valueString) { return Type.getType(valueString); } }; + private static final Pattern UNICODE_ESCAPE = Pattern.compile("\\\\u+([0-9a-fA-F]{1,4})"); private static final Map, LiteralType> valuesByClass = new HashMap<>(); private static final Map valuesByType = new HashMap<>(); static @@ -123,7 +180,7 @@ public static LiteralType from(Class clazz) if (valuesByClass.containsKey(clazz)) return valuesByClass.get(clazz); else - throw new IllegalArgumentException(clazz + " is not an int, long, float, double, String, or type reference"); + throw new IllegalArgumentException(clazz + " is not one of: " + describeValidTypes()); } public static LiteralType from(Type type) @@ -131,7 +188,14 @@ public static LiteralType from(Type type) if (valuesByType.containsKey(type)) return valuesByType.get(type); else - throw new IllegalArgumentException(type + " is not an int, float, long, double, String, or type reference"); + throw new IllegalArgumentException(type + " is not one of: " + describeValidTypes()); + } + + private static String describeValidTypes() + { + return Arrays.stream(values()) + .map(t -> t.name().toLowerCase(Locale.ROOT).replace('_', ' ')) + .collect(Collectors.joining(", ")); } public AbstractInsnNode createReturnInsn() diff --git a/src/main/java/daomephsta/unpick/impl/representations/AbstractConstantDefinition.java b/src/main/java/daomephsta/unpick/impl/representations/AbstractConstantDefinition.java index 50810df..f367d19 100644 --- a/src/main/java/daomephsta/unpick/impl/representations/AbstractConstantDefinition.java +++ b/src/main/java/daomephsta/unpick/impl/representations/AbstractConstantDefinition.java @@ -124,5 +124,10 @@ public ResolutionException(String message) { super(message); } + + public ResolutionException(String message, Throwable cause) + { + super(message, cause); + } } } diff --git a/src/main/java/daomephsta/unpick/impl/representations/FlagConstantGroup.java b/src/main/java/daomephsta/unpick/impl/representations/FlagConstantGroup.java index 9296bdf..07134f3 100644 --- a/src/main/java/daomephsta/unpick/impl/representations/FlagConstantGroup.java +++ b/src/main/java/daomephsta/unpick/impl/representations/FlagConstantGroup.java @@ -50,7 +50,7 @@ public boolean canReplace(Context context) public void generateReplacements(Context context) { Number literalNum = (Number) AbstractInsnNodes.getLiteralValue(context.getArgSeed()); - IntegerType integerType = IntegerType.from(literalNum.getClass()); + IntegerType integerType = IntegerType.from(literalNum); resolveAllConstants(context.getConstantResolver()); diff --git a/src/main/java/daomephsta/unpick/impl/representations/FlagDefinition.java b/src/main/java/daomephsta/unpick/impl/representations/FlagDefinition.java index 42cf6eb..55e8889 100644 --- a/src/main/java/daomephsta/unpick/impl/representations/FlagDefinition.java +++ b/src/main/java/daomephsta/unpick/impl/representations/FlagDefinition.java @@ -3,6 +3,7 @@ import org.objectweb.asm.Type; import daomephsta.unpick.constantmappers.datadriven.parser.UnpickSyntaxException; +import daomephsta.unpick.impl.IntegerType; /** * Represents a flag field. The value and descriptor may be @@ -42,11 +43,7 @@ protected Number parseValue(String valueString) { try { - if (descriptor == Type.INT_TYPE) - return Integer.parseInt(valueString); - else if (descriptor == Type.LONG_TYPE) - return Long.parseLong(valueString); - else throw new UnpickSyntaxException("Cannot parse value " + valueString + " with descriptor " + descriptor); + return IntegerType.from(descriptor).parse(valueString); } catch (IllegalArgumentException e) { @@ -57,10 +54,16 @@ else if (descriptor == Type.LONG_TYPE) @Override protected void setValue(Object value) throws ResolutionException { - if (value instanceof Long || value instanceof Integer) + try + { + // Will throw if value is not of an integral type + IntegerType.from(value); this.value = value; - else - throw new ResolutionException(this + " is not of a valid flag type. Flags must be ints or longs."); + } + catch (IllegalArgumentException e) + { + throw new ResolutionException(value + " is not of a valid flag type", e); + } } @Override diff --git a/src/test/java/daomephsta/unpick/tests/FlagUninliningTest.java b/src/test/java/daomephsta/unpick/tests/FlagUninliningTest.java index 7cbec3b..735ed33 100644 --- a/src/test/java/daomephsta/unpick/tests/FlagUninliningTest.java +++ b/src/test/java/daomephsta/unpick/tests/FlagUninliningTest.java @@ -19,7 +19,6 @@ import daomephsta.unpick.api.ConstantUninliner; import daomephsta.unpick.api.constantmappers.IConstantMapper; -import daomephsta.unpick.impl.AbstractInsnNodes; import daomephsta.unpick.impl.IntegerType; import daomephsta.unpick.impl.constantresolvers.ClasspathConstantResolver; import daomephsta.unpick.tests.lib.*; @@ -29,7 +28,17 @@ public class FlagUninliningTest { @SuppressWarnings("unused") private static class Constants - { + { + public static final byte BYTE_FLAG_BIT_0 = 1 << 0, + BYTE_FLAG_BIT_1 = 1 << 1, + BYTE_FLAG_BIT_2 = 1 << 2, + BYTE_FLAG_BIT_3 = 1 << 3; + + public static final short SHORT_FLAG_BIT_0 = 1 << 0, + SHORT_FLAG_BIT_1 = 1 << 1, + SHORT_FLAG_BIT_2 = 1 << 2, + SHORT_FLAG_BIT_3 = 1 << 3; + public static final int INT_FLAG_BIT_0 = 1 << 0, INT_FLAG_BIT_1 = 1 << 1, INT_FLAG_BIT_2 = 1 << 2, @@ -52,11 +61,43 @@ private static class Constants @SuppressWarnings("unused") private static class Methods { + private static void byteConsumer(byte test) {} + + private static void shortConsumer(short test) {} + private static void intConsumer(int test) {} private static void longConsumer(long test) {} } + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("byteFlagsProvider") + public void testKnownByteFlagsReturn(Byte testConstant, String[] expectedConstantCombination, String[] constantNames) + { + testKnownFlagsReturn(testConstant, expectedConstantCombination, constantNames); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("byteFlagsProvider") + public void testKnownByteFlagsParameter(Byte testConstant, String[] expectedConstantCombination, String[] constantNames) + { + testKnownFlagsParameter(testConstant, expectedConstantCombination, constantNames, "byteConsumer", "(B)V"); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("shortFlagsProvider") + public void testKnownShortFlagsReturn(Short testConstant, String[] expectedConstantCombination, String[] constantNames) + { + testKnownFlagsReturn(testConstant, expectedConstantCombination, constantNames); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("shortFlagsProvider") + public void testKnownShortFlagsParameter(Short testConstant, String[] expectedConstantCombination, String[] constantNames) + { + testKnownFlagsParameter(testConstant, expectedConstantCombination, constantNames, "shortConsumer", "(S)V"); + } + @ParameterizedTest(name = "{0} -> {1}") @MethodSource("intFlagsProvider") public void testKnownIntFlagsReturn(Integer testConstant, String[] expectedConstantCombination, String[] constantNames) @@ -96,7 +137,7 @@ private void testKnownFlagsParameter(Number testConstant, String[] expectedConst .add() .build(); - IntegerType integerType = IntegerType.from(testConstant.getClass()); + IntegerType integerType = IntegerType.from(testConstant); ConstantUninliner uninliner = new ConstantUninliner(mapper, new ClasspathConstantResolver()); MockMethod mockMethod = TestUtils.mockInvokeStatic(Methods.class, @@ -123,7 +164,7 @@ private void testKnownFlagsParameter(Number testConstant, String[] expectedConst private void testKnownFlagsReturn(Number testConstant, String[] expectedConstantCombination, String[] constantNames) { - IntegerType integerType = IntegerType.from(testConstant.getClass()); + IntegerType integerType = IntegerType.from(testConstant); MockMethod mock = MethodMocker.mock(integerType.getPrimitiveClass(), mv -> { integerType.appendLiteralPushInsn(mv, testConstant.longValue()); @@ -156,6 +197,34 @@ private void testKnownFlagsReturn(Number testConstant, String[] expectedConstant ASMAssertions.assertOpcode(mockMethod.instructions.get(j + 1), integerType.getOrOpcode()); } } + + @ParameterizedTest(name = "~{0} -> {1}") + @MethodSource("byteFlagsProvider") + public void testNegatedByteFlagsParameter(Byte testConstant, String[] expectedConstantCombination, String[] constantNames) + { + testNegatedFlagsParameter(testConstant, expectedConstantCombination, constantNames, "byteConsumer", "(B)V"); + } + + @ParameterizedTest(name = "~{0} -> {1}") + @MethodSource("byteFlagsProvider") + public void testNegatedByteFlagsReturn(Byte testConstant, String[] expectedConstantCombination, String[] constantNames) + { + testNegatedFlagsReturn(testConstant, expectedConstantCombination, constantNames); + } + + @ParameterizedTest(name = "~{0} -> {1}") + @MethodSource("shortFlagsProvider") + public void testNegatedShortFlagsParameter(Short testConstant, String[] expectedConstantCombination, String[] constantNames) + { + testNegatedFlagsParameter(testConstant, expectedConstantCombination, constantNames, "shortConsumer", "(B)V"); + } + + @ParameterizedTest(name = "~{0} -> {1}") + @MethodSource("shortFlagsProvider") + public void testNegatedShortFlagsReturn(Short testConstant, String[] expectedConstantCombination, String[] constantNames) + { + testNegatedFlagsReturn(testConstant, expectedConstantCombination, constantNames); + } @ParameterizedTest(name = "~{0} -> {1}") @MethodSource("intFlagsProvider") @@ -197,7 +266,7 @@ private void testNegatedFlagsParameter(Number testConstant, String[] expectedCon .build(); ConstantUninliner uninliner = new ConstantUninliner(mapper, new ClasspathConstantResolver()); - IntegerType integerType = IntegerType.from(testConstant.getClass()); + IntegerType integerType = IntegerType.from(testConstant); MockMethod mockMethod = MethodMocker.mock(void.class, mv -> { @@ -224,7 +293,7 @@ private void testNegatedFlagsParameter(Number testConstant, String[] expectedCon private void testNegatedFlagsReturn(Number testConstant, String[] expectedConstantCombination, String[] constantNames) { - IntegerType integerType = IntegerType.from(testConstant.getClass()); + IntegerType integerType = IntegerType.from(testConstant); MockMethod mock = MethodMocker.mock(integerType.getPrimitiveClass(), mv -> { mv.visitFieldInsn(Opcodes.GETSTATIC, "Foo", "bar", integerType.getTypeDescriptor()); @@ -259,6 +328,32 @@ private void testNegatedFlagsReturn(Number testConstant, String[] expectedConsta } } + @ParameterizedTest(name = "{0} -> {0}") + @ValueSource(bytes = {0b0000, 0b100000, 0b01000, 0b11000}) + public void testUnknownByteFlagsParameter(Byte testConstant) + { + testUnknownFlagsParameter(testConstant, "byteConsumer", "(B)V"); + } + @ParameterizedTest(name = "{0} -> {0}") + @ValueSource(bytes = {0b0000, 0b100000, 0b01000, 0b11000}) + public void testUnknownByteFlagsReturn(Byte testConstant) + { + testUnknownFlagsReturn(testConstant); + } + + @ParameterizedTest(name = "{0} -> {0}") + @ValueSource(shorts = {0b0000, 0b100000, 0b01000, 0b11000}) + public void testUnknownShortFlagsParameter(Short testConstant) + { + testUnknownFlagsParameter(testConstant, "shortConsumer", "(S)V"); + } + @ParameterizedTest(name = "{0} -> {0}") + @ValueSource(shorts = {0b0000, 0b100000, 0b01000, 0b11000}) + public void testUnknownShortFlagsReturn(Short testConstant) + { + testUnknownFlagsReturn(testConstant); + } + @ParameterizedTest(name = "{0} -> {0}") @ValueSource(ints = {0b0000, 0b100000, 0b01000, 0b11000}) public void testUnknownIntFlagsParameter(Integer testConstant) @@ -311,7 +406,7 @@ private void testUnknownFlagsParameter(Number constant, String constantConsumerN private void testUnknownFlagsReturn(Number testConstant) { - IntegerType integerType = IntegerType.from(testConstant.getClass()); + IntegerType integerType = IntegerType.from(testConstant); MockMethod mock = MethodMocker.mock(integerType.getPrimitiveClass(), mv -> { integerType.appendLiteralPushInsn(mv, testConstant.longValue()); @@ -338,6 +433,30 @@ private void testUnknownFlagsReturn(Number testConstant) ASMAssertions.assertOpcode(mockInvocation.instructions.get(1), integerType.getReturnOpcode()); } + private static Stream byteFlagsProvider() + { + String[] constantNames = {"BYTE_FLAG_BIT_0", "BYTE_FLAG_BIT_1", "BYTE_FLAG_BIT_2", "BYTE_FLAG_BIT_3"}; + return Stream.of + ( + Arguments.of((byte) 0b0100, new String[] {"BYTE_FLAG_BIT_2"}, constantNames), + Arguments.of((byte) 0b1100, new String[] {"BYTE_FLAG_BIT_2", "BYTE_FLAG_BIT_3"}, constantNames), + Arguments.of((byte) 0b1010, new String[] {"BYTE_FLAG_BIT_1", "BYTE_FLAG_BIT_3"}, constantNames), + Arguments.of((byte) 0b0111, new String[] {"BYTE_FLAG_BIT_0", "BYTE_FLAG_BIT_1", "BYTE_FLAG_BIT_2"}, constantNames) + ); + } + + private static Stream shortFlagsProvider() + { + String[] constantNames = {"SHORT_FLAG_BIT_0", "SHORT_FLAG_BIT_1", "SHORT_FLAG_BIT_2", "SHORT_FLAG_BIT_3"}; + return Stream.of + ( + Arguments.of((short) 0b0100, new String[] {"SHORT_FLAG_BIT_2"}, constantNames), + Arguments.of((short) 0b1100, new String[] {"SHORT_FLAG_BIT_2", "SHORT_FLAG_BIT_3"}, constantNames), + Arguments.of((short) 0b1010, new String[] {"SHORT_FLAG_BIT_1", "SHORT_FLAG_BIT_3"}, constantNames), + Arguments.of((short) 0b0111, new String[] {"SHORT_FLAG_BIT_0", "SHORT_FLAG_BIT_1", "SHORT_FLAG_BIT_2"}, constantNames) + ); + } + private static Stream intFlagsProvider() { String[] constantNames = {"INT_FLAG_BIT_0", "INT_FLAG_BIT_1", "INT_FLAG_BIT_2", "INT_FLAG_BIT_3"}; @@ -369,7 +488,7 @@ private void checkMockInvocationStructure(String constantConsumerName, int expectedInstructionCount = 3; assertEquals(expectedInstructionCount, mockInvocation.instructions.size(), String.format("Expected %d instructions, found %d", expectedInstructionCount, mockInvocation.instructions.size())); - assertEquals(expectedLiteralValue, AbstractInsnNodes.getLiteralValue(mockInvocation.instructions.get(invocationInsnIndex - 1))); + ASMAssertions.assertIsLiteral(mockInvocation.instructions.get(invocationInsnIndex - 1), expectedLiteralValue); ASMAssertions.assertInvokesMethod(mockInvocation.instructions.get(invocationInsnIndex), Methods.class, constantConsumerName, constantConsumerDescriptor); ASMAssertions.assertOpcode(mockInvocation.instructions.get(invocationInsnIndex + 1), RETURN); diff --git a/src/test/java/daomephsta/unpick/tests/SimpleConstantUninliningTest.java b/src/test/java/daomephsta/unpick/tests/SimpleConstantUninliningTest.java index 9d3cffe..cba70f5 100644 --- a/src/test/java/daomephsta/unpick/tests/SimpleConstantUninliningTest.java +++ b/src/test/java/daomephsta/unpick/tests/SimpleConstantUninliningTest.java @@ -6,27 +6,37 @@ import java.io.IOException; import java.util.stream.Stream; -import daomephsta.unpick.api.IClassResolver; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.*; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.MethodNode; import daomephsta.unpick.api.ConstantUninliner; +import daomephsta.unpick.api.IClassResolver; import daomephsta.unpick.api.constantmappers.IConstantMapper; -import daomephsta.unpick.impl.AbstractInsnNodes; import daomephsta.unpick.impl.LiteralType; import daomephsta.unpick.impl.constantresolvers.ClasspathConstantResolver; -import daomephsta.unpick.tests.lib.*; +import daomephsta.unpick.tests.lib.ASMAssertions; +import daomephsta.unpick.tests.lib.MethodMocker; import daomephsta.unpick.tests.lib.MethodMocker.MockMethod; +import daomephsta.unpick.tests.lib.MockConstantMapper; +import daomephsta.unpick.tests.lib.TestUtils; public class SimpleConstantUninliningTest { @SuppressWarnings("unused") private static class Methods { + private static void byteConsumer(byte test) {} + private static void intConsumer(int test) {} + + private static void shortConsumer(short test) {} + + private static void charConsumer(char test) {} private static void longConsumer(long test) {} @@ -36,10 +46,36 @@ private static void doubleConsumer(double test) {} private static void stringConsumer(String test) {} } - + @SuppressWarnings("unused") private static class Constants { + public static final byte BYTE_CONST_M1 = -1, + BYTE_CONST_0 = 0, + BYTE_CONST_1 = 1, + BYTE_CONST_2 = 2, + BYTE_CONST_3 = 3, + BYTE_CONST_4 = 4, + BYTE_CONST_5 = 5, + BYTE_CONST = 117; + + public static final short SHORT_CONST_M1 = -1, + SHORT_CONST_0 = 0, + SHORT_CONST_1 = 1, + SHORT_CONST_2 = 2, + SHORT_CONST_3 = 3, + SHORT_CONST_4 = 4, + SHORT_CONST_5 = 5, + SHORT_CONST = 257; + + public static final char CHAR_CONST_0 = '\0', + CHAR_CONST_1 = '\1', + CHAR_CONST_2 = '\2', + CHAR_CONST_3 = '\3', + CHAR_CONST_4 = '\4', + CHAR_CONST_5 = '\5', + CHAR_CONST = '\257'; + public static final int INT_CONST_M1 = -1, INT_CONST_0 = 0, INT_CONST_1 = 1, @@ -74,6 +110,131 @@ private static class Constants }; } + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("knownBytesProvider") + public void testKnownByteConstantsParameter(Byte constant, String constantName) + { + testKnownConstantParameter(constant, constantName, "byteConsumer", "(B)V"); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("knownBytesProvider") + public void testKnownByteConstantsReturn(Byte constant, String constantName) + { + testKnownConstantReturn(constant, constantName); + } + + private static Stream knownBytesProvider() + { + return Stream.of + ( + Arguments.of(Constants.BYTE_CONST_M1, "BYTE_CONST_M1"), + Arguments.of(Constants.BYTE_CONST_0, "BYTE_CONST_0"), + Arguments.of(Constants.BYTE_CONST_1, "BYTE_CONST_1"), + Arguments.of(Constants.BYTE_CONST_2, "BYTE_CONST_2"), + Arguments.of(Constants.BYTE_CONST_3, "BYTE_CONST_3"), + Arguments.of(Constants.BYTE_CONST_4, "BYTE_CONST_4"), + Arguments.of(Constants.BYTE_CONST_5, "BYTE_CONST_5") + ); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(bytes = {8, 13, 42, -1, -7, -23}) + public void testUnknownByteConstantsParameter(Byte constant) + { + testUnknownConstantParameter(constant, "byteConsumer", "(B)V"); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(bytes = {8, 13, 42, -1, -7, -23}) + public void testUnknownByteConstantsReturn(Byte constant) + { + testUnknownConstantReturn(constant); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("knownShortsProvider") + public void testKnownShortConstantsParameter(Short constant, String constantName) + { + testKnownConstantParameter(constant, constantName, "shortConsumer", "(S)V"); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("knownShortsProvider") + public void testKnownShortConstantsReturn(Short constant, String constantName) + { + testKnownConstantReturn(constant, constantName); + } + + private static Stream knownShortsProvider() + { + return Stream.of + ( + Arguments.of(Constants.SHORT_CONST_M1, "SHORT_CONST_M1"), + Arguments.of(Constants.SHORT_CONST_0, "SHORT_CONST_0"), + Arguments.of(Constants.SHORT_CONST_1, "SHORT_CONST_1"), + Arguments.of(Constants.SHORT_CONST_2, "SHORT_CONST_2"), + Arguments.of(Constants.SHORT_CONST_3, "SHORT_CONST_3"), + Arguments.of(Constants.SHORT_CONST_4, "SHORT_CONST_4"), + Arguments.of(Constants.SHORT_CONST_5, "SHORT_CONST_5") + ); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(shorts = {8, 13, 42, -1, -7, -23}) + public void testUnknownShortConstantsParameter(Short constant) + { + testUnknownConstantParameter(constant, "shortConsumer", "(S)V"); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(shorts = {8, 13, 42, -1, -7, -23}) + public void testUnknownShortConstantsReturn(Short constant) + { + testUnknownConstantReturn(constant); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("knownCharactersProvider") + public void testKnownCharacterConstantsParameter(Character constant, String constantName) + { + testKnownConstantParameter(constant, constantName, "charConsumer", "(C)V"); + } + + @ParameterizedTest(name = "{0} -> {1}") + @MethodSource("knownCharactersProvider") + public void testKnownCharacterConstantsReturn(Character constant, String constantName) + { + testKnownConstantReturn(constant, constantName); + } + + private static Stream knownCharactersProvider() + { + return Stream.of + ( + Arguments.of(Constants.CHAR_CONST_0, "CHAR_CONST_0"), + Arguments.of(Constants.CHAR_CONST_1, "CHAR_CONST_1"), + Arguments.of(Constants.CHAR_CONST_2, "CHAR_CONST_2"), + Arguments.of(Constants.CHAR_CONST_3, "CHAR_CONST_3"), + Arguments.of(Constants.CHAR_CONST_4, "CHAR_CONST_4"), + Arguments.of(Constants.CHAR_CONST_5, "CHAR_CONST_5") + ); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(chars = {'b', '#', ':', 'E', '$', '=', 's', '?', '/', '\\'}) + public void testUnknownCharacterConstantsParameter(Character constant) + { + testUnknownConstantParameter(constant, "charConsumer", "(C)V"); + } + + @ParameterizedTest(name = "{0}") + @ValueSource(chars = {'b', '#', ':', 'E', '$', '=', 's', '?', '/', '\\'}) + public void testUnknownCharacterConstantsReturn(Character constant) + { + testUnknownConstantReturn(constant); + } + @ParameterizedTest(name = "{0} -> {1}") @MethodSource("knownIntsProvider") public void testKnownIntConstantsParameter(Integer constant, String constantName) @@ -399,7 +560,7 @@ private void checkMockInvocationStructure(String constantConsumerName, int expectedInstructionCount = 3; assertEquals(expectedInstructionCount, mockInvocation.instructions.size(), String.format("Expected %d instructions, found %d", expectedInstructionCount, mockInvocation.instructions.size())); - assertEquals(expectedLiteralValue, AbstractInsnNodes.getLiteralValue(mockInvocation.instructions.get(invocationInsnIndex - 1))); + ASMAssertions.assertIsLiteral(mockInvocation.instructions.get(invocationInsnIndex - 1), expectedLiteralValue); ASMAssertions.assertInvokesMethod(mockInvocation.instructions.get(invocationInsnIndex), Methods.class, constantConsumerName, constantConsumerDescriptor); ASMAssertions.assertOpcode(mockInvocation.instructions.get(invocationInsnIndex + 1), RETURN);