diff --git a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java index 1ad767197..f4e45d0b2 100644 --- a/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/IntegrationTest.java @@ -18,7 +18,9 @@ import jadx.tests.api.utils.TestUtils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; @@ -71,8 +73,11 @@ public abstract class IntegrationTest extends TestUtils { } // don't unload class + System.out.println(cls.getCode()); + checkCode(cls); compile(cls); + runAutoCheck(clsName); return cls; } @@ -102,9 +107,65 @@ public abstract class IntegrationTest extends TestUtils { public boolean isFallbackMode() { return isFallback; } + + @Override + public boolean isShowInconsistentCode() { + return true; + } }, new File(outDir)); } + private void runAutoCheck(String clsName) { + try { + // run 'check' method from original class + Class origCls; + try { + origCls = Class.forName(clsName); + } catch (ClassNotFoundException e) { + // ignore + return; + } + Method checkMth; + try { + checkMth = origCls.getMethod("check"); + } catch (NoSuchMethodException e) { + // ignore + return; + } + if (!checkMth.getReturnType().equals(void.class) + || !Modifier.isPublic(checkMth.getModifiers()) + || Modifier.isStatic(checkMth.getModifiers())) { + fail("Wrong 'check' method"); + return; + } + try { + checkMth.invoke(origCls.newInstance()); + } catch (InvocationTargetException ie) { + rethrow("Java check failed", ie); + } + // run 'check' method from decompiled class + try { + invoke("check"); + } catch (InvocationTargetException ie) { + rethrow("Decompiled check failed", ie); + } + } catch (Exception e) { + e.printStackTrace(); + fail("Auto check exception: " + e.getMessage()); + } + } + + private void rethrow(String msg, InvocationTargetException ie) { + Throwable cause = ie.getCause(); + if (cause instanceof AssertionError) { + System.err.println(msg); + throw ((AssertionError) cause); + } else { + cause.printStackTrace(); + fail(msg + cause.getMessage()); + } + } + protected MethodNode getMethod(ClassNode cls, String method) { for (MethodNode mth : cls.getMethods()) { if (mth.getName().equals(method)) { @@ -133,7 +194,7 @@ public abstract class IntegrationTest extends TestUtils { return invoke(method, new Class[0]); } - public Object invoke(String method, Class[] types, Object... args) { + public Object invoke(String method, Class[] types, Object... args) throws Exception { Method mth = getReflectMethod(method, types); return invoke(mth, args); } @@ -149,16 +210,10 @@ public abstract class IntegrationTest extends TestUtils { return null; } - public Object invoke(Method mth, Object... args) { + public Object invoke(Method mth, Object... args) throws Exception { assertNotNull("dynamicCompiler not ready", dynamicCompiler); assertNotNull("unknown method", mth); - try { - return dynamicCompiler.invoke(mth, args); - } catch (Exception e) { - e.printStackTrace(); - fail(e.getMessage()); - } - return null; + return dynamicCompiler.invoke(mth, args); } public File getJarForClass(Class cls) throws IOException { diff --git a/jadx-core/src/test/java/jadx/tests/api/compiler/ClassFileManager.java b/jadx-core/src/test/java/jadx/tests/api/compiler/ClassFileManager.java index 676e22594..1fc7f8236 100644 --- a/jadx-core/src/test/java/jadx/tests/api/compiler/ClassFileManager.java +++ b/jadx-core/src/test/java/jadx/tests/api/compiler/ClassFileManager.java @@ -6,32 +6,71 @@ import javax.tools.JavaFileObject; import javax.tools.StandardJavaFileManager; import java.io.IOException; import java.security.SecureClassLoader; +import java.util.HashMap; +import java.util.Map; import static javax.tools.JavaFileObject.Kind; public class ClassFileManager extends ForwardingJavaFileManager { - private JavaClassObject jClsObject; + private DynamicClassLoader classLoader; public ClassFileManager(StandardJavaFileManager standardManager) { super(standardManager); + classLoader = new DynamicClassLoader(); } @Override public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) throws IOException { - jClsObject = new JavaClassObject(className, kind); - return jClsObject; + JavaClassObject clsObject = new JavaClassObject(className, kind); + classLoader.getClsMap().put(className, clsObject); + return clsObject; } @Override public ClassLoader getClassLoader(Location location) { - return new SecureClassLoader() { - @Override - protected Class findClass(String name) throws ClassNotFoundException { - byte[] clsBytes = jClsObject.getBytes(); - return super.defineClass(name, clsBytes, 0, clsBytes.length); + return classLoader; + } + + private class DynamicClassLoader extends SecureClassLoader { + private final Map clsMap = new HashMap(); + private final Map clsCache = new HashMap(); + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + Class cls = replaceClass(name); + if (cls != null) { + return cls; } - }; + return super.findClass(name); + } + + public Class loadClass(String name) throws ClassNotFoundException { + Class cls = replaceClass(name); + if (cls != null) { + return cls; + } + return super.loadClass(name); + } + + public Class replaceClass(String name) throws ClassNotFoundException { + Class cacheCls = clsCache.get(name); + if (cacheCls != null) { + return cacheCls; + } + JavaClassObject clsObject = clsMap.get(name); + if (clsObject == null) { + return null; + } + byte[] clsBytes = clsObject.getBytes(); + Class cls = super.defineClass(name, clsBytes, 0, clsBytes.length); + clsCache.put(name, cls); + return cls; + } + + public Map getClsMap() { + return clsMap; + } } } diff --git a/jadx-core/src/test/java/jadx/tests/api/compiler/DynamicCompiler.java b/jadx-core/src/test/java/jadx/tests/api/compiler/DynamicCompiler.java index 8f5999e7b..20d0d8497 100644 --- a/jadx-core/src/test/java/jadx/tests/api/compiler/DynamicCompiler.java +++ b/jadx-core/src/test/java/jadx/tests/api/compiler/DynamicCompiler.java @@ -38,9 +38,13 @@ public class DynamicCompiler { return Boolean.TRUE.equals(compilerTask.call()); } + private ClassLoader getClassLoader() { + return fileManager.getClassLoader(null); + } + private void makeInstance() throws Exception { String fullName = clsNode.getFullName(); - instance = fileManager.getClassLoader(null).loadClass(fullName).newInstance(); + instance = getClassLoader().loadClass(fullName).newInstance(); if (instance == null) { throw new NullPointerException("Instantiation failed"); } @@ -54,6 +58,9 @@ public class DynamicCompiler { } public Method getMethod(String method, Class[] types) throws Exception { + for (Class type : types) { + checkType(type); + } return getInstance().getClass().getMethod(method, types); } @@ -61,12 +68,17 @@ public class DynamicCompiler { return mth.invoke(getInstance(), args); } - public Object invoke(String method) throws Exception { - return invoke(method, new Class[0]); - } - - public Object invoke(String method, Class[] types, Object... args) throws Exception { - Method mth = getMethod(method, types); - return invoke(mth, args); + private Class checkType(Class type) throws ClassNotFoundException { + if (type.isPrimitive()) { + return type; + } + if (type.isArray()) { + return checkType(type.getComponentType()); + } + Class decompiledCls = getClassLoader().loadClass(type.getName()); + if (type != decompiledCls) { + throw new IllegalArgumentException("Internal test class cannot be used in method invoke"); + } + return decompiledCls; } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum.java b/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum.java index ce8cdd903..b171571f4 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum.java +++ b/jadx-core/src/test/java/jadx/tests/integration/enums/TestSwitchOverEnum.java @@ -3,8 +3,6 @@ package jadx.tests.integration.enums; import jadx.core.dex.nodes.ClassNode; import jadx.tests.api.IntegrationTest; -import java.lang.reflect.Method; - import org.junit.Test; import static jadx.tests.api.utils.JadxMatchers.countString; @@ -27,19 +25,19 @@ public class TestSwitchOverEnum extends IntegrationTest { return 0; } + public void check() { + assertEquals(1, testEnum(Count.ONE)); + assertEquals(2, testEnum(Count.TWO)); + assertEquals(0, testEnum(Count.THREE)); + } + @Test public void test() { ClassNode cls = getClassNode(TestSwitchOverEnum.class); String code = cls.getCode().toString(); - System.out.println(code); assertThat(code, countString(1, "synthetic")); assertThat(code, countString(2, "switch (c) {")); assertThat(code, countString(2, "case ONE:")); - - Method mth = getReflectMethod("testEnum", Count.class); - assertEquals(1, invoke(mth, Count.ONE)); - assertEquals(2, invoke(mth, Count.TWO)); - assertEquals(0, invoke(mth, Count.THREE)); } } diff --git a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakWithLabel.java b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakWithLabel.java index d44d011cb..83aef8a14 100644 --- a/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakWithLabel.java +++ b/jadx-core/src/test/java/jadx/tests/integration/loops/TestBreakWithLabel.java @@ -33,10 +33,9 @@ public class TestBreakWithLabel extends IntegrationTest { } @Test - public void test() { + public void test() throws Exception { ClassNode cls = getClassNode(TestCls.class); String code = cls.getCode().toString(); - System.out.println(code); assertThat(code, containsOne("loop0:")); assertThat(code, containsOne("break loop0;"));