1/*
2 * Copyright (C) 2008 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
18package com.android.tools.layoutlib.create;
19
20
21import org.junit.After;
22import org.junit.Before;
23import org.junit.Test;
24import org.objectweb.asm.ClassReader;
25import org.objectweb.asm.ClassVisitor;
26import org.objectweb.asm.FieldVisitor;
27import org.objectweb.asm.MethodVisitor;
28import org.objectweb.asm.Type;
29
30import java.io.ByteArrayOutputStream;
31import java.io.File;
32import java.io.IOException;
33import java.io.InputStream;
34import java.lang.reflect.InvocationTargetException;
35import java.lang.reflect.Method;
36import java.net.URL;
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.Enumeration;
40import java.util.HashSet;
41import java.util.Map;
42import java.util.Set;
43import java.util.TreeMap;
44import java.util.zip.ZipEntry;
45import java.util.zip.ZipFile;
46
47import static org.junit.Assert.assertArrayEquals;
48import static org.junit.Assert.assertEquals;
49import static org.junit.Assert.assertFalse;
50import static org.junit.Assert.assertNotNull;
51import static org.junit.Assert.assertTrue;
52
53/**
54 * Unit tests for some methods of {@link AsmGenerator}.
55 */
56public class AsmGeneratorTest {
57
58    private static final String[] EMPTY_STRING_ARRAY = new String[0];
59    private MockLog mLog;
60    private ArrayList<String> mOsJarPath;
61    private String mOsDestJar;
62    private File mTempFile;
63
64    // ASM internal name for the the class in java package that should be refactored.
65    private static final String JAVA_CLASS_NAME = "java/lang/JavaClass";
66
67    @Before
68    public void setUp() throws Exception {
69        mLog = new MockLog();
70        URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar");
71
72        mOsJarPath = new ArrayList<>();
73        //noinspection ConstantConditions
74        mOsJarPath.add(url.getFile());
75
76        mTempFile = File.createTempFile("mock", ".jar");
77        mOsDestJar = mTempFile.getAbsolutePath();
78        mTempFile.deleteOnExit();
79    }
80
81    @After
82    public void tearDown() throws Exception {
83        if (mTempFile != null) {
84            mTempFile.delete();
85            mTempFile = null;
86        }
87    }
88
89    @Test
90    public void testClassRenaming() throws IOException, LogAbortException {
91
92        ICreateInfo ci = new ICreateInfo() {
93            @Override
94            public Class<?>[] getInjectedClasses() {
95                // classes to inject in the final JAR
96                return new Class<?>[0];
97            }
98
99            @Override
100            public String[] getDelegateMethods() {
101                return EMPTY_STRING_ARRAY;
102            }
103
104            @Override
105            public String[] getDelegateClassNatives() {
106                return EMPTY_STRING_ARRAY;
107            }
108
109            @Override
110            public String[] getOverriddenMethods() {
111                // methods to force override
112                return EMPTY_STRING_ARRAY;
113            }
114
115            @Override
116            public String[] getRenamedClasses() {
117                // classes to rename (so that we can replace them)
118                return new String[] {
119                        "mock_android.view.View", "mock_android.view._Original_View",
120                        "not.an.actual.ClassName", "anoter.fake.NewClassName",
121                };
122            }
123
124            @Override
125            public String[] getJavaPkgClasses() {
126              return EMPTY_STRING_ARRAY;
127            }
128
129            @Override
130            public Set<String> getExcludedClasses() {
131                return null;
132            }
133
134            @Override
135            public String[] getDeleteReturns() {
136                 // methods deleted from their return type.
137                return EMPTY_STRING_ARRAY;
138            }
139
140            @Override
141            public String[] getPromotedFields() {
142                return EMPTY_STRING_ARRAY;
143            }
144
145            @Override
146            public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
147                return Collections.emptyMap();
148            }
149        };
150
151        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
152
153        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
154                null,                 // derived from
155                new String[] {        // include classes
156                    "**"
157                },
158                Collections.<String>emptySet() /* excluded classes */,
159                new String[]{} /* include files */);
160        aa.analyze();
161        agen.generate();
162
163        Set<String> notRenamed = agen.getClassesNotRenamed();
164        assertArrayEquals(new String[] { "not/an/actual/ClassName" }, notRenamed.toArray());
165
166    }
167
168    @Test
169    public void testClassRefactoring() throws IOException, LogAbortException {
170        ICreateInfo ci = new ICreateInfo() {
171            @Override
172            public Class<?>[] getInjectedClasses() {
173                // classes to inject in the final JAR
174                return new Class<?>[] {
175                        com.android.tools.layoutlib.create.dataclass.JavaClass.class
176                };
177            }
178
179            @Override
180            public String[] getDelegateMethods() {
181                return EMPTY_STRING_ARRAY;
182            }
183
184            @Override
185            public String[] getDelegateClassNatives() {
186                return EMPTY_STRING_ARRAY;
187            }
188
189            @Override
190            public String[] getOverriddenMethods() {
191                // methods to force override
192                return EMPTY_STRING_ARRAY;
193            }
194
195            @Override
196            public String[] getRenamedClasses() {
197                // classes to rename (so that we can replace them)
198                return EMPTY_STRING_ARRAY;
199            }
200
201            @Override
202            public String[] getJavaPkgClasses() {
203             // classes to refactor (so that we can replace them)
204                return new String[] {
205                        "java.lang.JavaClass", "com.android.tools.layoutlib.create.dataclass.JavaClass",
206                };
207            }
208
209            @Override
210            public Set<String> getExcludedClasses() {
211                return Collections.singleton("java.lang.JavaClass");
212            }
213
214            @Override
215            public String[] getDeleteReturns() {
216                 // methods deleted from their return type.
217                return EMPTY_STRING_ARRAY;
218            }
219
220            @Override
221            public String[] getPromotedFields() {
222                return EMPTY_STRING_ARRAY;
223            }
224
225            @Override
226            public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
227                return Collections.emptyMap();
228            }
229        };
230
231        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
232
233        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
234                null,                 // derived from
235                new String[] {        // include classes
236                    "**"
237                },
238                Collections.<String>emptySet(),
239                new String[] {        /* include files */
240                    "mock_android/data/data*"
241                });
242        aa.analyze();
243        agen.generate();
244        Map<String, ClassReader> output = new TreeMap<>();
245        Map<String, InputStream> filesFound = new TreeMap<>();
246        parseZip(mOsDestJar, output, filesFound);
247        boolean injectedClassFound = false;
248        for (ClassReader cr: output.values()) {
249            TestClassVisitor cv = new TestClassVisitor();
250            cr.accept(cv, 0);
251            injectedClassFound |= cv.mInjectedClassFound;
252        }
253        assertTrue(injectedClassFound);
254        assertArrayEquals(new String[] {"mock_android/data/dataFile"},
255                filesFound.keySet().toArray());
256    }
257
258    @Test
259    public void testClassExclusion() throws IOException, LogAbortException {
260        ICreateInfo ci = new ICreateInfo() {
261            @Override
262            public Class<?>[] getInjectedClasses() {
263                return new Class<?>[0];
264            }
265
266            @Override
267            public String[] getDelegateMethods() {
268                return EMPTY_STRING_ARRAY;
269            }
270
271            @Override
272            public String[] getDelegateClassNatives() {
273                return EMPTY_STRING_ARRAY;
274            }
275
276            @Override
277            public String[] getOverriddenMethods() {
278                // methods to force override
279                return EMPTY_STRING_ARRAY;
280            }
281
282            @Override
283            public String[] getRenamedClasses() {
284                // classes to rename (so that we can replace them)
285                return EMPTY_STRING_ARRAY;
286            }
287
288            @Override
289            public String[] getJavaPkgClasses() {
290                // classes to refactor (so that we can replace them)
291                return EMPTY_STRING_ARRAY;
292            }
293
294            @Override
295            public Set<String> getExcludedClasses() {
296                Set<String> set = new HashSet<>(2);
297                set.add("mock_android.dummy.InnerTest");
298                set.add("java.lang.JavaClass");
299                return set;
300            }
301
302            @Override
303            public String[] getDeleteReturns() {
304                // methods deleted from their return type.
305                return EMPTY_STRING_ARRAY;
306            }
307
308            @Override
309            public String[] getPromotedFields() {
310                return EMPTY_STRING_ARRAY;
311            }
312
313            @Override
314            public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
315                return Collections.emptyMap();
316            }
317        };
318
319        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
320        Set<String> excludedClasses = ci.getExcludedClasses();
321        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
322                null,                 // derived from
323                new String[] {        // include classes
324                        "**"
325                },
326                excludedClasses,
327                new String[] {        /* include files */
328                        "mock_android/data/data*"
329                });
330        aa.analyze();
331        agen.generate();
332        Map<String, ClassReader> output = new TreeMap<>();
333        Map<String, InputStream> filesFound = new TreeMap<>();
334        parseZip(mOsDestJar, output, filesFound);
335        for (String s : output.keySet()) {
336            assertFalse(excludedClasses.contains(s));
337        }
338        assertArrayEquals(new String[] {"mock_android/data/dataFile"},
339                filesFound.keySet().toArray());
340    }
341
342    @Test
343    public void testMethodInjection() throws IOException, LogAbortException,
344            ClassNotFoundException, IllegalAccessException, InstantiationException,
345            NoSuchMethodException, InvocationTargetException {
346        ICreateInfo ci = new ICreateInfo() {
347            @Override
348            public Class<?>[] getInjectedClasses() {
349                return new Class<?>[0];
350            }
351
352            @Override
353            public String[] getDelegateMethods() {
354                return EMPTY_STRING_ARRAY;
355            }
356
357            @Override
358            public String[] getDelegateClassNatives() {
359                return EMPTY_STRING_ARRAY;
360            }
361
362            @Override
363            public String[] getOverriddenMethods() {
364                // methods to force override
365                return EMPTY_STRING_ARRAY;
366            }
367
368            @Override
369            public String[] getRenamedClasses() {
370                // classes to rename (so that we can replace them)
371                return EMPTY_STRING_ARRAY;
372            }
373
374            @Override
375            public String[] getJavaPkgClasses() {
376                // classes to refactor (so that we can replace them)
377                return EMPTY_STRING_ARRAY;
378            }
379
380            @Override
381            public Set<String> getExcludedClasses() {
382                return Collections.emptySet();
383            }
384
385            @Override
386            public String[] getDeleteReturns() {
387                // methods deleted from their return type.
388                return EMPTY_STRING_ARRAY;
389            }
390
391            @Override
392            public String[] getPromotedFields() {
393                return EMPTY_STRING_ARRAY;
394            }
395
396            @Override
397            public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
398                return Collections.singletonMap("mock_android.util.EmptyArray",
399                        InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER);
400            }
401        };
402
403        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
404        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
405                null,                 // derived from
406                new String[] {        // include classes
407                        "**"
408                },
409                ci.getExcludedClasses(),
410                new String[] {        /* include files */
411                        "mock_android/data/data*"
412                });
413        aa.analyze();
414        agen.generate();
415        Map<String, ClassReader> output = new TreeMap<>();
416        Map<String, InputStream> filesFound = new TreeMap<>();
417        parseZip(mOsDestJar, output, filesFound);
418        final String modifiedClass = "mock_android.util.EmptyArray";
419        final String modifiedClassPath = modifiedClass.replace('.', '/').concat(".class");
420        ZipFile zipFile = new ZipFile(mOsDestJar);
421        ZipEntry entry = zipFile.getEntry(modifiedClassPath);
422        assertNotNull(entry);
423        final byte[] bytes;
424        try (InputStream inputStream = zipFile.getInputStream(entry)) {
425            bytes = getByteArray(inputStream);
426        }
427        ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) {
428            @Override
429            protected Class<?> findClass(String name) throws ClassNotFoundException {
430                if (name.equals(modifiedClass)) {
431                    return defineClass(null, bytes, 0, bytes.length);
432                }
433                throw new ClassNotFoundException(name + " not found.");
434            }
435        };
436        Class<?> emptyArrayClass = classLoader.loadClass(modifiedClass);
437        Object emptyArrayInstance = emptyArrayClass.newInstance();
438        Method method = emptyArrayClass.getMethod("getFrameworkClassLoader");
439        Object cl = method.invoke(emptyArrayInstance);
440        assertEquals(classLoader, cl);
441    }
442
443    private static byte[] getByteArray(InputStream stream) throws IOException {
444        ByteArrayOutputStream bos = new ByteArrayOutputStream();
445        byte[] buffer = new byte[1024];
446        int read;
447        while ((read = stream.read(buffer, 0, buffer.length)) > -1) {
448            bos.write(buffer, 0, read);
449        }
450        return bos.toByteArray();
451    }
452
453    private void parseZip(String jarPath,
454            Map<String, ClassReader> classes,
455            Map<String, InputStream> filesFound) throws IOException {
456
457            ZipFile zip = new ZipFile(jarPath);
458            Enumeration<? extends ZipEntry> entries = zip.entries();
459            ZipEntry entry;
460            while (entries.hasMoreElements()) {
461                entry = entries.nextElement();
462                if (entry.getName().endsWith(".class")) {
463                    ClassReader cr = new ClassReader(zip.getInputStream(entry));
464                    String className = classReaderToClassName(cr);
465                    classes.put(className, cr);
466                } else {
467                    filesFound.put(entry.getName(), zip.getInputStream(entry));
468                }
469            }
470
471    }
472
473    private String classReaderToClassName(ClassReader classReader) {
474        if (classReader == null) {
475            return null;
476        } else {
477            return classReader.getClassName().replace('/', '.');
478        }
479    }
480
481    private class TestClassVisitor extends ClassVisitor {
482
483        boolean mInjectedClassFound = false;
484
485        TestClassVisitor() {
486            super(Main.ASM_VERSION);
487        }
488
489        @Override
490        public void visit(int version, int access, String name, String signature,
491                String superName, String[] interfaces) {
492            assertTrue(!getBase(name).equals(JAVA_CLASS_NAME));
493            if (name.equals("com/android/tools/layoutlib/create/dataclass/JavaClass")) {
494                mInjectedClassFound = true;
495            }
496            super.visit(version, access, name, signature, superName, interfaces);
497        }
498
499        @Override
500        public FieldVisitor visitField(int access, String name, String desc,
501                String signature, Object value) {
502            assertTrue(testType(Type.getType(desc)));
503            return super.visitField(access, name, desc, signature, value);
504        }
505
506        @SuppressWarnings("hiding")
507        @Override
508        public MethodVisitor visitMethod(int access, String name, String desc,
509                String signature, String[] exceptions) {
510            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
511            return new MethodVisitor(Main.ASM_VERSION, mv) {
512
513                @Override
514                public void visitFieldInsn(int opcode, String owner, String name,
515                        String desc) {
516                    assertTrue(!getBase(owner).equals(JAVA_CLASS_NAME));
517                    assertTrue(testType(Type.getType(desc)));
518                    super.visitFieldInsn(opcode, owner, name, desc);
519                }
520
521                @Override
522                public void visitLdcInsn(Object cst) {
523                    if (cst instanceof Type) {
524                        assertTrue(testType((Type)cst));
525                    }
526                    super.visitLdcInsn(cst);
527                }
528
529                @Override
530                public void visitTypeInsn(int opcode, String type) {
531                    assertTrue(!getBase(type).equals(JAVA_CLASS_NAME));
532                    super.visitTypeInsn(opcode, type);
533                }
534
535                @Override
536                public void visitMethodInsn(int opcode, String owner, String name,
537                        String desc, boolean itf) {
538                    assertTrue(!getBase(owner).equals(JAVA_CLASS_NAME));
539                    assertTrue(testType(Type.getType(desc)));
540                    super.visitMethodInsn(opcode, owner, name, desc, itf);
541                }
542
543            };
544        }
545
546        private boolean testType(Type type) {
547            int sort = type.getSort();
548            if (sort == Type.OBJECT) {
549                assertTrue(!getBase(type.getInternalName()).equals(JAVA_CLASS_NAME));
550            } else if (sort == Type.ARRAY) {
551                assertTrue(!getBase(type.getElementType().getInternalName())
552                        .equals(JAVA_CLASS_NAME));
553            } else if (sort == Type.METHOD) {
554                boolean r = true;
555                for (Type t : type.getArgumentTypes()) {
556                    r &= testType(t);
557                }
558                return r & testType(type.getReturnType());
559            }
560            return true;
561        }
562
563        private String getBase(String className) {
564            if (className == null) {
565                return null;
566            }
567            int pos = className.indexOf('$');
568            if (pos > 0) {
569                return className.substring(0, pos);
570            }
571            return className;
572        }
573    }
574}
575