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