| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| |
| package com.android.tools.layoutlib.create; |
| |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.objectweb.asm.ClassReader; |
| import org.objectweb.asm.ClassVisitor; |
| import org.objectweb.asm.FieldVisitor; |
| import org.objectweb.asm.MethodVisitor; |
| import org.objectweb.asm.Type; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| |
| import static org.junit.Assert.assertArrayEquals; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| /** |
| * Unit tests for some methods of {@link AsmGenerator}. |
| */ |
| public class AsmGeneratorTest { |
| private MockLog mLog; |
| private ArrayList<String> mOsJarPath; |
| private String mOsDestJar; |
| private File mTempFile; |
| |
| // ASM internal name for the the class in java package that should be refactored. |
| private static final String JAVA_CLASS_NAME = "notjava.lang.JavaClass"; |
| |
| @Before |
| public void setUp() throws Exception { |
| mLog = new MockLog(); |
| URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); |
| |
| mOsJarPath = new ArrayList<>(); |
| mOsJarPath.add(url.getFile()); |
| |
| mTempFile = File.createTempFile("mock", ".jar"); |
| mOsDestJar = mTempFile.getAbsolutePath(); |
| mTempFile.deleteOnExit(); |
| } |
| |
| @After |
| public void tearDown() { |
| if (mTempFile != null) { |
| //noinspection ResultOfMethodCallIgnored |
| mTempFile.delete(); |
| mTempFile = null; |
| } |
| } |
| |
| @Test |
| public void testClassRenaming() throws IOException { |
| |
| ICreateInfo ci = new CreateInfoAdapter() { |
| @Override |
| public String[] getRenamedClasses() { |
| // classes to rename (so that we can replace them) |
| return new String[] { |
| "mock_android.view.View", "mock_android.view._Original_View", |
| "not.an.actual.ClassName", "another.fake.NewClassName", |
| }; |
| } |
| }; |
| |
| AsmGenerator agen = new AsmGenerator(mLog, ci); |
| |
| AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, |
| null, // derived from |
| new String[] { // include classes |
| "**" |
| }, |
| new String[]{} /* excluded classes */, |
| new String[]{} /* include files */); |
| agen.setAnalysisResult(aa.analyze()); |
| agen.generate(); |
| |
| Set<String> notRenamed = agen.getClassesNotRenamed(); |
| assertArrayEquals(new String[] { "not/an/actual/ClassName" }, notRenamed.toArray()); |
| |
| } |
| |
| @Test |
| public void testJavaClassRefactoring() throws IOException { |
| ICreateInfo ci = new CreateInfoAdapter() { |
| @Override |
| public Class<?>[] getInjectedClasses() { |
| // classes to inject in the final JAR |
| return new Class<?>[] { |
| com.android.tools.layoutlib.create.dataclass.JavaClass.class |
| }; |
| } |
| |
| @Override |
| public String[] getJavaPkgClasses() { |
| // classes to refactor (so that we can replace them) |
| return new String[] { |
| JAVA_CLASS_NAME, "com.android.tools.layoutlib.create.dataclass.JavaClass", |
| }; |
| } |
| |
| @Override |
| public String[] getExcludedClasses() { |
| return new String[]{JAVA_CLASS_NAME}; |
| } |
| }; |
| |
| AsmGenerator agen = new AsmGenerator(mLog, ci); |
| |
| AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, |
| null, // derived from |
| new String[] { // include classes |
| "**" |
| }, |
| new String[]{}, |
| new String[] { /* include files */ |
| "mock_android/data/data*" |
| }); |
| agen.setAnalysisResult(aa.analyze()); |
| Map<String, byte[]> output = agen.generate(); |
| RecordingClassVisitor cv = new RecordingClassVisitor(); |
| for (Map.Entry<String, byte[]> entry: output.entrySet()) { |
| if (!entry.getKey().endsWith(".class")) continue; |
| ClassReader cr = new ClassReader(entry.getValue()); |
| cr.accept(cv, 0); |
| } |
| assertTrue(cv.mVisitedClasses.contains( |
| "com/android/tools/layoutlib/create/dataclass/JavaClass")); |
| assertFalse(cv.mVisitedClasses.contains( |
| JAVA_CLASS_NAME)); |
| assertArrayEquals(new String[] {"mock_android/data/dataFile"}, |
| findFileNames(output)); |
| } |
| |
| @Test |
| public void testClassRefactoring() throws IOException { |
| ICreateInfo ci = new CreateInfoAdapter() { |
| @Override |
| public Class<?>[] getInjectedClasses() { |
| // classes to inject in the final JAR |
| return new Class<?>[] { |
| com.android.tools.layoutlib.create.dataclass.JavaClass.class |
| }; |
| } |
| |
| @Override |
| public String[] getRefactoredClasses() { |
| // classes to refactor (so that we can replace them) |
| return new String[] { |
| "mock_android.view.View", "mock_android.view._Original_View", |
| }; |
| } |
| }; |
| |
| AsmGenerator agen = new AsmGenerator(mLog, ci); |
| |
| AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, |
| null, // derived from |
| new String[] { // include classes |
| "**" |
| }, |
| new String[]{}, |
| new String[] {}); |
| agen.setAnalysisResult(aa.analyze()); |
| Map<String, byte[]> output = agen.generate(); |
| RecordingClassVisitor cv = new RecordingClassVisitor(); |
| for (byte[] classContent: output.values()) { |
| ClassReader cr = new ClassReader(classContent); |
| cr.accept(cv, 0); |
| } |
| assertTrue(cv.mVisitedClasses.contains( |
| "mock_android/view/_Original_View")); |
| assertFalse(cv.mVisitedClasses.contains( |
| "mock_android/view/View")); |
| } |
| |
| @Test |
| public void testClassExclusion() throws IOException { |
| ICreateInfo ci = new CreateInfoAdapter() { |
| @Override |
| public String[] getExcludedClasses() { |
| return new String[] { |
| "mock_android.dummy2.*", |
| "mock_android.dummy.**", |
| "mock_android.util.NotNeeded", |
| JAVA_CLASS_NAME |
| }; |
| } |
| }; |
| |
| AsmGenerator agen = new AsmGenerator(mLog, ci); |
| AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, |
| null, // derived from |
| new String[] { // include classes |
| "**" |
| }, |
| ci.getExcludedClasses(), |
| new String[] { /* include files */ |
| "mock_android/data/data*" |
| }); |
| agen.setAnalysisResult(aa.analyze()); |
| Map<String, byte[]> output = agen.generate(); |
| // Everything in .dummy.** should be filtered |
| // Only things is .dummy2.* should be filtered |
| assertArrayEquals(new String[] { |
| "mock_android.dummy2.keep.DoNotRemove", |
| "mock_android.util.EmptyArray", |
| "mock_android.view.View", |
| "mock_android.view.ViewGroup", |
| "mock_android.view.ViewGroup$LayoutParams", |
| "mock_android.view.ViewGroup$MarginLayoutParams", |
| "mock_android.widget.LinearLayout", |
| "mock_android.widget.LinearLayout$LayoutParams", |
| "mock_android.widget.TableLayout", |
| "mock_android.widget.TableLayout$LayoutParams"}, |
| findClassNames(output) |
| ); |
| assertArrayEquals(new String[] {"mock_android/data/dataFile"}, |
| findFileNames(output)); |
| } |
| |
| @Test |
| public void testMethodInjection() throws IOException, ClassNotFoundException, |
| IllegalAccessException, InstantiationException, |
| NoSuchMethodException, InvocationTargetException { |
| ICreateInfo ci = new CreateInfoAdapter() { |
| @Override |
| public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { |
| return Collections.singletonMap("mock_android.util.EmptyArray", |
| InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER); |
| } |
| }; |
| |
| AsmGenerator agen = new AsmGenerator(mLog, ci); |
| AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, |
| null, // derived from |
| new String[] { // include classes |
| "**" |
| }, |
| ci.getExcludedClasses(), |
| new String[] { /* include files */ |
| "mock_android/data/data*" |
| }); |
| agen.setAnalysisResult(aa.analyze()); |
| JarUtil.createJar(new FileOutputStream(mOsDestJar), agen.generate()); |
| |
| final String modifiedClass = "mock_android.util.EmptyArray"; |
| final String modifiedClassPath = modifiedClass.replace('.', '/').concat(".class"); |
| ZipFile zipFile = new ZipFile(mOsDestJar); |
| ZipEntry entry = zipFile.getEntry(modifiedClassPath); |
| assertNotNull(entry); |
| final byte[] bytes; |
| try (InputStream inputStream = zipFile.getInputStream(entry)) { |
| bytes = getByteArray(inputStream); |
| } |
| ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) { |
| @Override |
| protected Class<?> findClass(String name) throws ClassNotFoundException { |
| if (name.equals(modifiedClass)) { |
| return defineClass(null, bytes, 0, bytes.length); |
| } |
| throw new ClassNotFoundException(name + " not found."); |
| } |
| }; |
| Class<?> emptyArrayClass = classLoader.loadClass(modifiedClass); |
| Object emptyArrayInstance = emptyArrayClass.newInstance(); |
| Method method = emptyArrayClass.getMethod("getFrameworkClassLoader"); |
| Object cl = method.invoke(emptyArrayInstance); |
| assertEquals(classLoader, cl); |
| } |
| |
| private static byte[] getByteArray(InputStream stream) throws IOException { |
| ByteArrayOutputStream bos = new ByteArrayOutputStream(); |
| byte[] buffer = new byte[1024]; |
| int read; |
| while ((read = stream.read(buffer, 0, buffer.length)) > -1) { |
| bos.write(buffer, 0, read); |
| } |
| return bos.toByteArray(); |
| } |
| |
| |
| private static String[] findClassNames(Map<String, byte[]> content) { |
| return content.entrySet().stream() |
| .filter(entry -> entry.getKey().endsWith(".class")) |
| .map(entry -> classReaderToClassName(new ClassReader(entry.getValue()))) |
| .sorted() |
| .toArray(String[]::new); |
| } |
| |
| private static String[] findFileNames(Map<String, byte[]> content) { |
| return content.keySet().stream() |
| .filter(entry -> !entry.endsWith(".class")) |
| .sorted() |
| .toArray(String[]::new); |
| } |
| |
| private static String classReaderToClassName(ClassReader classReader) { |
| if (classReader == null) { |
| return null; |
| } else { |
| return classReader.getClassName().replace('/', '.'); |
| } |
| } |
| |
| /** |
| * {@link ClassVisitor} that records every class that sees. |
| */ |
| private static class RecordingClassVisitor extends ClassVisitor { |
| private Set<String> mVisitedClasses = new HashSet<>(); |
| |
| private RecordingClassVisitor() { |
| super(Main.ASM_VERSION); |
| } |
| |
| private void addClass(String className) { |
| if (className == null) { |
| return; |
| } |
| |
| int pos = className.indexOf('$'); |
| if (pos > 0) { |
| // For inner classes, add also the base class |
| mVisitedClasses.add(className.substring(0, pos)); |
| } |
| mVisitedClasses.add(className); |
| } |
| |
| @Override |
| public void visit(int version, int access, String name, String signature, String superName, |
| String[] interfaces) { |
| addClass(superName); |
| Arrays.stream(interfaces).forEach(this::addClass); |
| } |
| |
| private void processType(Type type) { |
| switch (type.getSort()) { |
| case Type.OBJECT: |
| addClass(type.getInternalName()); |
| break; |
| case Type.ARRAY: |
| addClass(type.getElementType().getInternalName()); |
| break; |
| case Type.METHOD: |
| processType(type.getReturnType()); |
| Arrays.stream(type.getArgumentTypes()).forEach(this::processType); |
| break; |
| } |
| } |
| |
| @Override |
| public FieldVisitor visitField(int access, String name, String desc, String signature, |
| Object value) { |
| processType(Type.getType(desc)); |
| return super.visitField(access, name, desc, signature, value); |
| } |
| |
| @Override |
| public MethodVisitor visitMethod(int access, String name, String desc, String signature, |
| String[] exceptions) { |
| MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); |
| return new MethodVisitor(Main.ASM_VERSION, mv) { |
| |
| @Override |
| public void visitFieldInsn(int opcode, String owner, String name, String desc) { |
| addClass(owner); |
| processType(Type.getType(desc)); |
| super.visitFieldInsn(opcode, owner, name, desc); |
| } |
| |
| @Override |
| public void visitLdcInsn(Object cst) { |
| if (cst instanceof Type) { |
| processType((Type) cst); |
| } |
| super.visitLdcInsn(cst); |
| } |
| |
| @Override |
| public void visitTypeInsn(int opcode, String type) { |
| addClass(type); |
| super.visitTypeInsn(opcode, type); |
| } |
| |
| @Override |
| public void visitMethodInsn(int opcode, String owner, String name, String desc, |
| boolean itf) { |
| addClass(owner); |
| processType(Type.getType(desc)); |
| super.visitMethodInsn(opcode, owner, name, desc, itf); |
| } |
| |
| }; |
| } |
| } |
| } |