1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 
18 package com.android.tools.layoutlib.create;
19 
20 
21 import com.android.tools.layoutlib.create.CreateInfo.SystemLoadLibraryReplacer;
22 import com.android.tools.layoutlib.create.ICreateInfo.MethodInformation;
23 import com.android.tools.layoutlib.create.ICreateInfo.MethodReplacer;
24 
25 import org.junit.After;
26 import org.junit.Before;
27 import org.junit.Test;
28 import org.objectweb.asm.ClassReader;
29 import org.objectweb.asm.ClassVisitor;
30 import org.objectweb.asm.FieldVisitor;
31 import org.objectweb.asm.MethodVisitor;
32 import org.objectweb.asm.Type;
33 
34 import java.io.ByteArrayOutputStream;
35 import java.io.File;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.lang.reflect.InvocationTargetException;
40 import java.lang.reflect.Method;
41 import java.net.URL;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collections;
45 import java.util.HashSet;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Set;
49 import java.util.zip.ZipEntry;
50 import java.util.zip.ZipFile;
51 
52 import static org.junit.Assert.assertArrayEquals;
53 import static org.junit.Assert.assertEquals;
54 import static org.junit.Assert.assertFalse;
55 import static org.junit.Assert.assertNotNull;
56 import static org.junit.Assert.assertTrue;
57 
58 /**
59  * Unit tests for some methods of {@link AsmGenerator}.
60  */
61 public class AsmGeneratorTest {
62     private MockLog mLog;
63     private ArrayList<String> mOsJarPath;
64     private String mOsDestJar;
65     private File mTempFile;
66 
67     // ASM internal name for the class in java package that should be refactored.
68     private static final String JAVA_CLASS_NAME = "notjava.lang.JavaClass";
69 
70     @Before
setUp()71     public void setUp() throws Exception {
72         mLog = new MockLog();
73         URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar");
74 
75         mOsJarPath = new ArrayList<>();
76         mOsJarPath.add(url.getFile());
77 
78         mTempFile = File.createTempFile("mock", ".jar");
79         mOsDestJar = mTempFile.getAbsolutePath();
80         mTempFile.deleteOnExit();
81     }
82 
83     @After
tearDown()84     public void tearDown() {
85         if (mTempFile != null) {
86             //noinspection ResultOfMethodCallIgnored
87             mTempFile.delete();
88             mTempFile = null;
89         }
90     }
91 
92     @Test
testClassRenaming()93     public void testClassRenaming() throws IOException {
94 
95         ICreateInfo ci = new CreateInfoAdapter() {
96             @Override
97             public String[] getRenamedClasses() {
98                 // classes to rename (so that we can replace them)
99                 return new String[] {
100                         "mock_android.view.View", "mock_android.view._Original_View",
101                         "not.an.actual.ClassName", "another.fake.NewClassName",
102                 };
103             }
104         };
105 
106         AsmGenerator agen = new AsmGenerator(mLog, ci);
107 
108         AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath,
109                 null,                 // derived from
110                 new String[] {        // include classes
111                     "**"
112                 },
113                 new String[]{}  /* excluded classes */,
114                 new String[]{}, /* include files */
115                 new MethodReplacer[] {});
116         agen.setAnalysisResult(aa.analyze());
117         agen.generate();
118 
119         Set<String> notRenamed = agen.getClassesNotRenamed();
120         assertArrayEquals(new String[] { "not/an/actual/ClassName" }, notRenamed.toArray());
121 
122     }
123 
124     @Test
testJavaClassRefactoring()125     public void testJavaClassRefactoring() throws IOException {
126         ICreateInfo ci = new CreateInfoAdapter() {
127             @Override
128             public Class<?>[] getInjectedClasses() {
129                 // classes to inject in the final JAR
130                 return new Class<?>[] {
131                         com.android.tools.layoutlib.create.dataclass.JavaClass.class
132                 };
133             }
134 
135             @Override
136             public String[] getJavaPkgClasses() {
137              // classes to refactor (so that we can replace them)
138                 return new String[] {
139                         JAVA_CLASS_NAME, "com.android.tools.layoutlib.create.dataclass.JavaClass",
140                 };
141             }
142 
143             @Override
144             public String[] getExcludedClasses() {
145                 return new String[]{JAVA_CLASS_NAME};
146             }
147         };
148 
149         AsmGenerator agen = new AsmGenerator(mLog, ci);
150 
151         AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath,
152                 null,                 // derived from
153                 new String[] {        // include classes
154                     "**"
155                 },
156                 new String[]{},
157                 new String[] {        /* include files */
158                     "mock_android/data/data*"
159                 },
160                 new MethodReplacer[] {} /* method replacers */);
161         agen.setAnalysisResult(aa.analyze());
162         Map<String, byte[]> output = agen.generate();
163         RecordingClassVisitor cv = new RecordingClassVisitor();
164         for (Map.Entry<String, byte[]> entry: output.entrySet()) {
165             if (!entry.getKey().endsWith(".class")) continue;
166             ClassReader cr = new ClassReader(entry.getValue());
167             cr.accept(cv, 0);
168         }
169         assertTrue(cv.mVisitedClasses.contains(
170                 "com/android/tools/layoutlib/create/dataclass/JavaClass"));
171         assertFalse(cv.mVisitedClasses.contains(
172                 JAVA_CLASS_NAME));
173         assertArrayEquals(new String[] {"mock_android/data/dataFile"},
174                 findFileNames(output));
175     }
176 
177     @Test
testClassRefactoring()178     public void testClassRefactoring() throws IOException {
179         ICreateInfo ci = new CreateInfoAdapter() {
180             @Override
181             public Class<?>[] getInjectedClasses() {
182                 // classes to inject in the final JAR
183                 return new Class<?>[] {
184                         com.android.tools.layoutlib.create.dataclass.JavaClass.class
185                 };
186             }
187 
188             @Override
189             public String[] getRefactoredClasses() {
190                 // classes to refactor (so that we can replace them)
191                 return new String[] {
192                         "mock_android.view.View", "mock_android.view._Original_View",
193                 };
194             }
195         };
196 
197         AsmGenerator agen = new AsmGenerator(mLog, ci);
198 
199         AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath,
200                 null,                 // derived from
201                 new String[] {        // include classes
202                         "**"
203                 },
204                 new String[]{},
205                 new String[] {},
206                 new MethodReplacer[] {});
207         agen.setAnalysisResult(aa.analyze());
208         Map<String, byte[]> output = agen.generate();
209         RecordingClassVisitor cv = new RecordingClassVisitor();
210         for (byte[] classContent: output.values()) {
211             ClassReader cr = new ClassReader(classContent);
212             cr.accept(cv, 0);
213         }
214         assertTrue(cv.mVisitedClasses.contains(
215                 "mock_android/view/_Original_View"));
216         assertFalse(cv.mVisitedClasses.contains(
217                 "mock_android/view/View"));
218     }
219 
220     @Test
testClassExclusion()221     public void testClassExclusion() throws IOException {
222         ICreateInfo ci = new CreateInfoAdapter() {
223             @Override
224             public String[] getExcludedClasses() {
225                 return new String[] {
226                         "mock_android.fake2.*",
227                         "mock_android.fake.**",
228                         "mock_android.util.NotNeeded",
229                         JAVA_CLASS_NAME
230                 };
231             }
232         };
233 
234         AsmGenerator agen = new AsmGenerator(mLog, ci);
235         AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath,
236                 null,                 // derived from
237                 new String[] {        // include classes
238                         "**"
239                 },
240                 ci.getExcludedClasses(),
241                 new String[] {        /* include files */
242                         "mock_android/data/data*"
243                 },
244                 new MethodReplacer[] {});
245         agen.setAnalysisResult(aa.analyze());
246         Map<String, byte[]> output = agen.generate();
247         // Everything in .fake.** should be filtered
248         // Only things is .fake2.* should be filtered
249         assertArrayEquals(new String[] {
250                 "mock_android.fake2.keep.DoNotRemove",
251                 "mock_android.util.EmptyArray",
252                 "mock_android.view.LibLoader",
253                 "mock_android.view.View",
254                 "mock_android.view.ViewGroup",
255                 "mock_android.view.ViewGroup$LayoutParams",
256                 "mock_android.view.ViewGroup$MarginLayoutParams",
257                 "mock_android.widget.LinearLayout",
258                 "mock_android.widget.LinearLayout$LayoutParams",
259                 "mock_android.widget.TableLayout",
260                 "mock_android.widget.TableLayout$LayoutParams"},
261                 findClassNames(output)
262         );
263         assertArrayEquals(new String[] {"mock_android/data/dataFile"},
264                 findFileNames(output));
265     }
266 
267     @Test
testMethodInjection()268     public void testMethodInjection() throws IOException, ClassNotFoundException,
269             IllegalAccessException, InstantiationException,
270             NoSuchMethodException, InvocationTargetException {
271         ICreateInfo ci = new CreateInfoAdapter() {
272             @Override
273             public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
274                 return Collections.singletonMap("mock_android.util.EmptyArray",
275                         InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER);
276             }
277         };
278 
279         AsmGenerator agen = new AsmGenerator(mLog, ci);
280         AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath,
281                 null,                 // derived from
282                 new String[] {        // include classes
283                         "**"
284                 },
285                 ci.getExcludedClasses(),
286                 new String[] {        /* include files */
287                         "mock_android/data/data*"
288                 },
289                 new MethodReplacer[] {});
290         agen.setAnalysisResult(aa.analyze());
291         JarUtil.createJar(new FileOutputStream(mOsDestJar), agen.generate());
292 
293         final String modifiedClass = "mock_android.util.EmptyArray";
294         final String modifiedClassPath = modifiedClass.replace('.', '/').concat(".class");
295         ZipFile zipFile = new ZipFile(mOsDestJar);
296         ZipEntry entry = zipFile.getEntry(modifiedClassPath);
297         assertNotNull(entry);
298         final byte[] bytes;
299         try (InputStream inputStream = zipFile.getInputStream(entry)) {
300             bytes = getByteArray(inputStream);
301         }
302         ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) {
303             @Override
304             protected Class<?> findClass(String name) throws ClassNotFoundException {
305                 if (name.equals(modifiedClass)) {
306                     return defineClass(null, bytes, 0, bytes.length);
307                 }
308                 throw new ClassNotFoundException(name + " not found.");
309             }
310         };
311         Class<?> emptyArrayClass = classLoader.loadClass(modifiedClass);
312         Object emptyArrayInstance = emptyArrayClass.newInstance();
313         Method method = emptyArrayClass.getMethod("getFrameworkClassLoader");
314         Object cl = method.invoke(emptyArrayInstance);
315         assertEquals(classLoader, cl);
316     }
317 
318     @Test
testMethodVisitor_loadLibraryReplacer()319     public void testMethodVisitor_loadLibraryReplacer() throws IOException {
320         final List<String> isNeeded = new ArrayList<>();
321         final List<String> replaced = new ArrayList<>();
322         MethodReplacer recordingReplacer = new MethodReplacer() {
323             private final MethodReplacer loadLibraryReplacer = new SystemLoadLibraryReplacer();
324             private final List<String> isNeededList = isNeeded;
325             private final List<String> replacedList = replaced;
326 
327             @Override
328             public boolean isNeeded(String owner, String name, String desc, String sourceClass) {
329                 boolean res = loadLibraryReplacer.isNeeded(owner, name, desc, sourceClass);
330                 if (res) {
331                     isNeededList.add(sourceClass + "->" + owner + "." + name);
332                 }
333                 return res;
334             }
335 
336             @Override
337             public void replace(MethodInformation mi) {
338                 replacedList.add(mi.owner + "." + mi.name);
339             }
340         };
341         MethodReplacer[] replacers = new MethodReplacer[] { recordingReplacer };
342 
343         ICreateInfo ci = new CreateInfoAdapter() {
344             @Override
345             public MethodReplacer[] getMethodReplacers() {
346                 return replacers;
347             }
348         };
349 
350         AsmGenerator agen = new AsmGenerator(mLog, ci);
351         AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath,
352                 null,                 // derived from
353                 new String[] {        // include classes
354                         "**"
355                 },
356                 new String[] {},
357                 new String[] {},
358                 replacers);
359         agen.setAnalysisResult(aa.analyze());
360 
361         assertTrue(isNeeded.contains("mock_android/view/LibLoader->java/lang/System.loadLibrary"));
362         assertTrue(replaced.isEmpty());
363 
364         agen.generate();
365 
366         assertTrue(isNeeded.contains("mock_android/view/LibLoader->java/lang/System.loadLibrary"));
367         assertTrue(replaced.contains("java/lang/System.loadLibrary"));
368     }
369 
getByteArray(InputStream stream)370     private static byte[] getByteArray(InputStream stream) throws IOException {
371         ByteArrayOutputStream bos = new ByteArrayOutputStream();
372         byte[] buffer = new byte[1024];
373         int read;
374         while ((read = stream.read(buffer, 0, buffer.length)) > -1) {
375             bos.write(buffer, 0, read);
376         }
377         return bos.toByteArray();
378     }
379 
380 
findClassNames(Map<String, byte[]> content)381     private static String[] findClassNames(Map<String, byte[]> content) {
382         return content.entrySet().stream()
383                 .filter(entry -> entry.getKey().endsWith(".class"))
384                 .map(entry -> classReaderToClassName(new ClassReader(entry.getValue())))
385                 .sorted()
386                 .toArray(String[]::new);
387     }
388 
findFileNames(Map<String, byte[]> content)389     private static String[] findFileNames(Map<String, byte[]> content) {
390         return content.keySet().stream()
391                 .filter(entry -> !entry.endsWith(".class"))
392                 .sorted()
393                 .toArray(String[]::new);
394     }
395 
classReaderToClassName(ClassReader classReader)396     private static String classReaderToClassName(ClassReader classReader) {
397         if (classReader == null) {
398             return null;
399         } else {
400             return classReader.getClassName().replace('/', '.');
401         }
402     }
403 
404     /**
405      * {@link ClassVisitor} that records every class that sees.
406      */
407     private static class RecordingClassVisitor extends ClassVisitor {
408         private Set<String> mVisitedClasses = new HashSet<>();
409 
RecordingClassVisitor()410         private RecordingClassVisitor() {
411             super(Main.ASM_VERSION);
412         }
413 
addClass(String className)414         private void addClass(String className) {
415             if (className == null) {
416                 return;
417             }
418 
419             int pos = className.indexOf('$');
420             if (pos > 0) {
421                 // For inner classes, add also the base class
422                 mVisitedClasses.add(className.substring(0, pos));
423             }
424             mVisitedClasses.add(className);
425         }
426 
427         @Override
visit(int version, int access, String name, String signature, String superName, String[] interfaces)428         public void visit(int version, int access, String name, String signature, String superName,
429                 String[] interfaces) {
430             addClass(superName);
431             Arrays.stream(interfaces).forEach(this::addClass);
432         }
433 
processType(Type type)434         private void processType(Type type) {
435             switch (type.getSort()) {
436                 case Type.OBJECT:
437                     addClass(type.getInternalName());
438                     break;
439                 case Type.ARRAY:
440                     addClass(type.getElementType().getInternalName());
441                     break;
442                 case Type.METHOD:
443                     processType(type.getReturnType());
444                     Arrays.stream(type.getArgumentTypes()).forEach(this::processType);
445                     break;
446             }
447         }
448 
449         @Override
visitField(int access, String name, String desc, String signature, Object value)450         public FieldVisitor visitField(int access, String name, String desc, String signature,
451                 Object value) {
452             processType(Type.getType(desc));
453             return super.visitField(access, name, desc, signature, value);
454         }
455 
456         @Override
visitMethod(int access, String name, String desc, String signature, String[] exceptions)457         public MethodVisitor visitMethod(int access, String name, String desc, String signature,
458                 String[] exceptions) {
459             MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
460             return new MethodVisitor(Main.ASM_VERSION, mv) {
461 
462                 @Override
463                 public void visitFieldInsn(int opcode, String owner, String name, String desc) {
464                     addClass(owner);
465                     processType(Type.getType(desc));
466                     super.visitFieldInsn(opcode, owner, name, desc);
467                 }
468 
469                 @Override
470                 public void visitLdcInsn(Object cst) {
471                     if (cst instanceof Type) {
472                         processType((Type) cst);
473                     }
474                     super.visitLdcInsn(cst);
475                 }
476 
477                 @Override
478                 public void visitTypeInsn(int opcode, String type) {
479                     addClass(type);
480                     super.visitTypeInsn(opcode, type);
481                 }
482 
483                 @Override
484                 public void visitMethodInsn(int opcode, String owner, String name, String desc,
485                         boolean itf) {
486                     addClass(owner);
487                     processType(Type.getType(desc));
488                     super.visitMethodInsn(opcode, owner, name, desc, itf);
489                 }
490 
491             };
492         }
493     }
494 }
495