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.Arrays;
39import java.util.Collections;
40import java.util.Enumeration;
41import java.util.HashSet;
42import java.util.Map;
43import java.util.Set;
44import java.util.TreeMap;
45import java.util.zip.ZipEntry;
46import java.util.zip.ZipFile;
47
48import static org.junit.Assert.assertArrayEquals;
49import static org.junit.Assert.assertEquals;
50import static org.junit.Assert.assertFalse;
51import static org.junit.Assert.assertNotNull;
52import static org.junit.Assert.assertTrue;
53
54/**
55 * Unit tests for some methods of {@link AsmGenerator}.
56 */
57public class AsmGeneratorTest {
58    private MockLog mLog;
59    private ArrayList<String> mOsJarPath;
60    private String mOsDestJar;
61    private File mTempFile;
62
63    // ASM internal name for the the class in java package that should be refactored.
64    private static final String JAVA_CLASS_NAME = "java/lang/JavaClass";
65
66    @Before
67    public void setUp() throws Exception {
68        mLog = new MockLog();
69        URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar");
70
71        mOsJarPath = new ArrayList<>();
72        //noinspection ConstantConditions
73        mOsJarPath.add(url.getFile());
74
75        mTempFile = File.createTempFile("mock", ".jar");
76        mOsDestJar = mTempFile.getAbsolutePath();
77        mTempFile.deleteOnExit();
78    }
79
80    @After
81    public void tearDown() throws Exception {
82        if (mTempFile != null) {
83            //noinspection ResultOfMethodCallIgnored
84            mTempFile.delete();
85            mTempFile = null;
86        }
87    }
88
89    @Test
90    public void testClassRenaming() throws IOException, LogAbortException {
91
92        ICreateInfo ci = new CreateInfoAdapter() {
93            @Override
94            public String[] getRenamedClasses() {
95                // classes to rename (so that we can replace them)
96                return new String[] {
97                        "mock_android.view.View", "mock_android.view._Original_View",
98                        "not.an.actual.ClassName", "anoter.fake.NewClassName",
99                };
100            }
101        };
102
103        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
104
105        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
106                null,                 // derived from
107                new String[] {        // include classes
108                    "**"
109                },
110                Collections.emptySet() /* excluded classes */,
111                new String[]{} /* include files */);
112        aa.analyze();
113        agen.generate();
114
115        Set<String> notRenamed = agen.getClassesNotRenamed();
116        assertArrayEquals(new String[] { "not/an/actual/ClassName" }, notRenamed.toArray());
117
118    }
119
120    @Test
121    public void testJavaClassRefactoring() throws IOException, LogAbortException {
122        ICreateInfo ci = new CreateInfoAdapter() {
123            @Override
124            public Class<?>[] getInjectedClasses() {
125                // classes to inject in the final JAR
126                return new Class<?>[] {
127                        com.android.tools.layoutlib.create.dataclass.JavaClass.class
128                };
129            }
130
131            @Override
132            public String[] getJavaPkgClasses() {
133             // classes to refactor (so that we can replace them)
134                return new String[] {
135                        "java.lang.JavaClass", "com.android.tools.layoutlib.create.dataclass.JavaClass",
136                };
137            }
138
139            @Override
140            public Set<String> getExcludedClasses() {
141                return Collections.singleton("java.lang.JavaClass");
142            }
143        };
144
145        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
146
147        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
148                null,                 // derived from
149                new String[] {        // include classes
150                    "**"
151                },
152                Collections.emptySet(),
153                new String[] {        /* include files */
154                    "mock_android/data/data*"
155                });
156        aa.analyze();
157        agen.generate();
158        Map<String, ClassReader> output = new TreeMap<>();
159        Map<String, InputStream> filesFound = new TreeMap<>();
160        parseZip(mOsDestJar, output, filesFound);
161        RecordingClassVisitor cv = new RecordingClassVisitor();
162        for (ClassReader cr: output.values()) {
163            cr.accept(cv, 0);
164        }
165        assertTrue(cv.mVisitedClasses.contains(
166                "com/android/tools/layoutlib/create/dataclass/JavaClass"));
167        assertFalse(cv.mVisitedClasses.contains(
168                JAVA_CLASS_NAME));
169        assertArrayEquals(new String[] {"mock_android/data/dataFile"},
170                filesFound.keySet().toArray());
171    }
172
173    @Test
174    public void testClassRefactoring() throws IOException, LogAbortException {
175        ICreateInfo ci = new CreateInfoAdapter() {
176            @Override
177            public Class<?>[] getInjectedClasses() {
178                // classes to inject in the final JAR
179                return new Class<?>[] {
180                        com.android.tools.layoutlib.create.dataclass.JavaClass.class
181                };
182            }
183
184            @Override
185            public String[] getRefactoredClasses() {
186                // classes to refactor (so that we can replace them)
187                return new String[] {
188                        "mock_android.view.View", "mock_android.view._Original_View",
189                };
190            }
191        };
192
193        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
194
195        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
196                null,                 // derived from
197                new String[] {        // include classes
198                        "**"
199                },
200                Collections.emptySet(),
201                new String[] {});
202        aa.analyze();
203        agen.generate();
204        Map<String, ClassReader> output = new TreeMap<>();
205        parseZip(mOsDestJar, output, new TreeMap<>());
206        RecordingClassVisitor cv = new RecordingClassVisitor();
207        for (ClassReader cr: output.values()) {
208            cr.accept(cv, 0);
209        }
210        assertTrue(cv.mVisitedClasses.contains(
211                "mock_android/view/_Original_View"));
212        assertFalse(cv.mVisitedClasses.contains(
213                "mock_android/view/View"));
214    }
215
216    @Test
217    public void testClassExclusion() throws IOException, LogAbortException {
218        ICreateInfo ci = new CreateInfoAdapter() {
219            @Override
220            public Set<String> getExcludedClasses() {
221                Set<String> set = new HashSet<>(2);
222                set.add("mock_android.dummy.InnerTest");
223                set.add("java.lang.JavaClass");
224                return set;
225            }
226        };
227
228        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
229        Set<String> excludedClasses = ci.getExcludedClasses();
230        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
231                null,                 // derived from
232                new String[] {        // include classes
233                        "**"
234                },
235                excludedClasses,
236                new String[] {        /* include files */
237                        "mock_android/data/data*"
238                });
239        aa.analyze();
240        agen.generate();
241        Map<String, ClassReader> output = new TreeMap<>();
242        Map<String, InputStream> filesFound = new TreeMap<>();
243        parseZip(mOsDestJar, output, filesFound);
244        for (String s : output.keySet()) {
245            assertFalse(excludedClasses.contains(s));
246        }
247        assertArrayEquals(new String[] {"mock_android/data/dataFile"},
248                filesFound.keySet().toArray());
249    }
250
251    @Test
252    public void testMethodInjection() throws IOException, LogAbortException,
253            ClassNotFoundException, IllegalAccessException, InstantiationException,
254            NoSuchMethodException, InvocationTargetException {
255        ICreateInfo ci = new CreateInfoAdapter() {
256            @Override
257            public Map<String, InjectMethodRunnable> getInjectedMethodsMap() {
258                return Collections.singletonMap("mock_android.util.EmptyArray",
259                        InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER);
260            }
261        };
262
263        AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci);
264        AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen,
265                null,                 // derived from
266                new String[] {        // include classes
267                        "**"
268                },
269                ci.getExcludedClasses(),
270                new String[] {        /* include files */
271                        "mock_android/data/data*"
272                });
273        aa.analyze();
274        agen.generate();
275        Map<String, ClassReader> output = new TreeMap<>();
276        Map<String, InputStream> filesFound = new TreeMap<>();
277        parseZip(mOsDestJar, output, filesFound);
278        final String modifiedClass = "mock_android.util.EmptyArray";
279        final String modifiedClassPath = modifiedClass.replace('.', '/').concat(".class");
280        ZipFile zipFile = new ZipFile(mOsDestJar);
281        ZipEntry entry = zipFile.getEntry(modifiedClassPath);
282        assertNotNull(entry);
283        final byte[] bytes;
284        try (InputStream inputStream = zipFile.getInputStream(entry)) {
285            bytes = getByteArray(inputStream);
286        }
287        ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) {
288            @Override
289            protected Class<?> findClass(String name) throws ClassNotFoundException {
290                if (name.equals(modifiedClass)) {
291                    return defineClass(null, bytes, 0, bytes.length);
292                }
293                throw new ClassNotFoundException(name + " not found.");
294            }
295        };
296        Class<?> emptyArrayClass = classLoader.loadClass(modifiedClass);
297        Object emptyArrayInstance = emptyArrayClass.newInstance();
298        Method method = emptyArrayClass.getMethod("getFrameworkClassLoader");
299        Object cl = method.invoke(emptyArrayInstance);
300        assertEquals(classLoader, cl);
301    }
302
303    private static byte[] getByteArray(InputStream stream) throws IOException {
304        ByteArrayOutputStream bos = new ByteArrayOutputStream();
305        byte[] buffer = new byte[1024];
306        int read;
307        while ((read = stream.read(buffer, 0, buffer.length)) > -1) {
308            bos.write(buffer, 0, read);
309        }
310        return bos.toByteArray();
311    }
312
313    private void parseZip(String jarPath,
314            Map<String, ClassReader> classes,
315            Map<String, InputStream> filesFound) throws IOException {
316
317            ZipFile zip = new ZipFile(jarPath);
318            Enumeration<? extends ZipEntry> entries = zip.entries();
319            ZipEntry entry;
320            while (entries.hasMoreElements()) {
321                entry = entries.nextElement();
322                if (entry.getName().endsWith(".class")) {
323                    ClassReader cr = new ClassReader(zip.getInputStream(entry));
324                    String className = classReaderToClassName(cr);
325                    classes.put(className, cr);
326                } else {
327                    filesFound.put(entry.getName(), zip.getInputStream(entry));
328                }
329            }
330
331    }
332
333    private String classReaderToClassName(ClassReader classReader) {
334        if (classReader == null) {
335            return null;
336        } else {
337            return classReader.getClassName().replace('/', '.');
338        }
339    }
340
341    /**
342     * {@link ClassVisitor} that records every class that sees.
343     */
344    private static class RecordingClassVisitor extends ClassVisitor {
345        private Set<String> mVisitedClasses = new HashSet<>();
346
347        private RecordingClassVisitor() {
348            super(Main.ASM_VERSION);
349        }
350
351        private void addClass(String className) {
352            if (className == null) {
353                return;
354            }
355
356            int pos = className.indexOf('$');
357            if (pos > 0) {
358                // For inner classes, add also the base class
359                mVisitedClasses.add(className.substring(0, pos));
360            }
361            mVisitedClasses.add(className);
362        }
363
364        @Override
365        public void visit(int version, int access, String name, String signature, String superName,
366                String[] interfaces) {
367            addClass(superName);
368            Arrays.stream(interfaces).forEach(this::addClass);
369        }
370
371        private void processType(Type type) {
372            switch (type.getSort()) {
373                case Type.OBJECT:
374                    addClass(type.getInternalName());
375                    break;
376                case Type.ARRAY:
377                    addClass(type.getElementType().getInternalName());
378                    break;
379                case Type.METHOD:
380                    processType(type.getReturnType());
381                    Arrays.stream(type.getArgumentTypes()).forEach(this::processType);
382                    break;
383            }
384        }
385
386        @Override
387        public FieldVisitor visitField(int access, String name, String desc, String signature,
388                Object value) {
389            processType(Type.getType(desc));
390            return super.visitField(access, name, desc, signature, value);
391        }
392
393        @Override
394        public MethodVisitor visitMethod(int access, String name, String desc, String signature,
395                String[] exceptions) {
396            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
397            return new MethodVisitor(Main.ASM_VERSION, mv) {
398
399                @Override
400                public void visitFieldInsn(int opcode, String owner, String name, String desc) {
401                    addClass(owner);
402                    processType(Type.getType(desc));
403                    super.visitFieldInsn(opcode, owner, name, desc);
404                }
405
406                @Override
407                public void visitLdcInsn(Object cst) {
408                    if (cst instanceof Type) {
409                        processType((Type) cst);
410                    }
411                    super.visitLdcInsn(cst);
412                }
413
414                @Override
415                public void visitTypeInsn(int opcode, String type) {
416                    addClass(type);
417                    super.visitTypeInsn(opcode, type);
418                }
419
420                @Override
421                public void visitMethodInsn(int opcode, String owner, String name, String desc,
422                        boolean itf) {
423                    addClass(owner);
424                    processType(Type.getType(desc));
425                    super.visitMethodInsn(opcode, owner, name, desc, itf);
426                }
427
428            };
429        }
430    }
431}
432