blob: efefcbc8cb89807c32d84d7b12ab3ca9f94f9b24 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.bytecode;
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
/**
* Java application that takes in an input jar, performs a series of bytecode transformations,
* and generates an output jar.
*
* Two types of transformations are performed:
* 1) Enabling assertions via {@link AssertionEnablerClassAdapter}
* 2) Providing support for custom resources via {@link CustomResourcesClassAdapter}
*/
class ByteCodeProcessor {
private static final String CLASS_FILE_SUFFIX = ".class";
private static final String TEMPORARY_FILE_SUFFIX = ".temp";
private static final int BUFFER_SIZE = 16384;
private static boolean sVerbose;
private static boolean sIsPrebuilt;
private static boolean sShouldAssert;
private static boolean sShouldUseCustomResources;
private static boolean sShouldUseThreadAnnotations;
private static boolean sShouldCheckClassPath;
private static ClassLoader sDirectClassPathClassLoader;
private static ClassLoader sFullClassPathClassLoader;
private static Set<String> sFullClassPathJarPaths;
private static String sGenerateClassDepsPath;
private static Set<String> sSplitCompatClassNames;
private static ClassPathValidator sValidator;
private static class EntryDataPair {
private final ZipEntry mEntry;
private final byte[] mData;
private EntryDataPair(ZipEntry mEntry, byte[] mData) {
this.mEntry = mEntry;
this.mData = mData;
}
private static EntryDataPair create(String zipPath, byte[] data) {
ZipEntry entry = new ZipEntry(zipPath);
entry.setMethod(ZipEntry.STORED);
entry.setTime(0);
entry.setSize(data.length);
CRC32 crc = new CRC32();
crc.update(data);
entry.setCrc(crc.getValue());
return new EntryDataPair(entry, data);
}
}
private static EntryDataPair processEntry(ZipEntry entry, byte[] data)
throws ClassPathValidator.ClassNotLoadedException {
// Copy all non-.class files to the output jar.
if (entry.isDirectory() || !entry.getName().endsWith(CLASS_FILE_SUFFIX)) {
return new EntryDataPair(entry, data);
}
ClassReader reader = new ClassReader(data);
if (sShouldCheckClassPath) {
sValidator.validateClassPathsAndOutput(reader, sDirectClassPathClassLoader,
sFullClassPathClassLoader, sFullClassPathJarPaths, sIsPrebuilt, sVerbose);
}
ClassWriter writer;
if (sShouldUseCustomResources) {
// Use the COMPUTE_FRAMES flag to have asm figure out the stack map frames.
// This is necessary because GCMBaseIntentService in android_gcm_java contains
// incorrect stack map frames. This option slows down processing time by 2x.
writer = new CustomClassLoaderClassWriter(
sFullClassPathClassLoader, reader, COMPUTE_FRAMES);
} else {
writer = new ClassWriter(reader, 0);
}
ClassVisitor chain = writer;
/* DEBUGGING:
To see the bytecode for a specific class:
if (entry.getName().contains("YourClassName")) {
chain = new TraceClassVisitor(chain, new PrintWriter(System.out));
}
To see objectweb.asm code that will generate bytecode for a given class:
java -cp "third_party/ow2_asm/lib/asm-5.0.1.jar:third_party/ow2_asm/lib/"\
"asm-util-5.0.1.jar:out/Debug/lib.java/jar_containing_yourclass.jar" \
org.objectweb.asm.util.ASMifier org.package.YourClassName
*/
if (sShouldUseThreadAnnotations) {
chain = new ThreadAssertionClassAdapter(chain);
}
if (sShouldAssert) {
chain = new AssertionEnablerClassAdapter(chain);
}
if (sShouldUseCustomResources) {
chain = new CustomResourcesClassAdapter(
chain, reader.getClassName(), reader.getSuperName(), sFullClassPathClassLoader);
}
if (!sSplitCompatClassNames.isEmpty()) {
chain = new SplitCompatClassAdapter(
chain, sSplitCompatClassNames, sFullClassPathClassLoader);
}
reader.accept(chain, 0);
byte[] patchedByteCode = writer.toByteArray();
return EntryDataPair.create(entry.getName(), patchedByteCode);
}
private static void process(String inputJarPath, String outputJarPath)
throws ClassPathValidator.ClassNotLoadedException, ExecutionException,
InterruptedException {
String tempJarPath = outputJarPath + TEMPORARY_FILE_SUFFIX;
ExecutorService executorService =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
try (ZipInputStream inputStream = new ZipInputStream(
new BufferedInputStream(new FileInputStream(inputJarPath)));
ZipOutputStream tempStream = new ZipOutputStream(
new BufferedOutputStream(new FileOutputStream(tempJarPath)))) {
List<Future<EntryDataPair>> list = new ArrayList<>();
while (true) {
ZipEntry entry = inputStream.getNextEntry();
if (entry == null) {
break;
}
byte[] data = readAllBytes(inputStream);
list.add(executorService.submit(() -> processEntry(entry, data)));
}
executorService.shutdown(); // This is essential in order to avoid waiting infinitely.
// Write the zip file entries in order to preserve determinism.
for (Future<EntryDataPair> futurePair : list) {
EntryDataPair pair = futurePair.get();
tempStream.putNextEntry(pair.mEntry);
tempStream.write(pair.mData);
tempStream.closeEntry();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
Path src = Paths.get(tempJarPath);
Path dest = Paths.get(outputJarPath);
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ioException) {
throw new RuntimeException(ioException);
}
if (sValidator.hasErrors()) {
System.err.println("Direct classpath is incomplete. To fix, add deps on the "
+ "GN target(s) that provide:");
for (Map.Entry<String, Map<String, Set<String>>> entry :
sValidator.getErrors().entrySet()) {
printValidationError(System.err, entry.getKey(), entry.getValue());
}
System.exit(1);
}
}
private static void printValidationError(
PrintStream out, String jarName, Map<String, Set<String>> missingClasses) {
out.print(" * ");
out.println(jarName);
int i = 0;
final int numErrorsPerJar = 2;
// The list of missing classes is non-exhaustive because each class that fails to validate
// reports only the first missing class.
for (Map.Entry<String, Set<String>> entry : missingClasses.entrySet()) {
String missingClass = entry.getKey();
Set<String> filesThatNeededIt = entry.getValue();
out.print(" * ");
if (i == numErrorsPerJar) {
out.print(String.format("And %d more...", missingClasses.size() - numErrorsPerJar));
break;
}
out.print(missingClass.replace('/', '.'));
out.print(" (needed by ");
out.print(filesThatNeededIt.iterator().next().replace('/', '.'));
if (filesThatNeededIt.size() > 1) {
out.print(String.format(" and %d more", filesThatNeededIt.size() - 1));
}
out.println(")");
i++;
}
}
private static byte[] readAllBytes(InputStream inputStream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int numRead = 0;
byte[] data = new byte[BUFFER_SIZE];
while ((numRead = inputStream.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, numRead);
}
return buffer.toByteArray();
}
/**
* Loads a list of jars and returns a ClassLoader capable of loading all classes found in the
* given jars.
*/
static ClassLoader loadJars(Collection<String> paths) {
URL[] jarUrls = new URL[paths.size()];
int i = 0;
for (String path : paths) {
try {
jarUrls[i++] = new File(path).toURI().toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
return new URLClassLoader(jarUrls);
}
public static void main(String[] args) throws ClassPathValidator.ClassNotLoadedException,
ExecutionException, InterruptedException {
// Invoke this script using //build/android/gyp/bytecode_processor.py
int currIndex = 0;
String inputJarPath = args[currIndex++];
String outputJarPath = args[currIndex++];
sVerbose = args[currIndex++].equals("--verbose");
sIsPrebuilt = args[currIndex++].equals("--is-prebuilt");
sShouldAssert = args[currIndex++].equals("--enable-assert");
sShouldUseCustomResources = args[currIndex++].equals("--enable-custom-resources");
sShouldUseThreadAnnotations = args[currIndex++].equals("--enable-thread-annotations");
sShouldCheckClassPath = args[currIndex++].equals("--enable-check-class-path");
sGenerateClassDepsPath = args[currIndex++];
int sdkJarsLength = Integer.parseInt(args[currIndex++]);
List<String> sdkJarPaths =
Arrays.asList(Arrays.copyOfRange(args, currIndex, currIndex + sdkJarsLength));
currIndex += sdkJarsLength;
int directJarsLength = Integer.parseInt(args[currIndex++]);
ArrayList<String> directClassPathJarPaths = new ArrayList<>();
directClassPathJarPaths.add(inputJarPath);
directClassPathJarPaths.addAll(sdkJarPaths);
directClassPathJarPaths.addAll(
Arrays.asList(Arrays.copyOfRange(args, currIndex, currIndex + directJarsLength)));
currIndex += directJarsLength;
sDirectClassPathClassLoader = loadJars(directClassPathJarPaths);
// Load list of class names that need to be fixed.
int splitCompatClassNamesLength = Integer.parseInt(args[currIndex++]);
sSplitCompatClassNames = new HashSet<>();
sSplitCompatClassNames.addAll(Arrays.asList(
Arrays.copyOfRange(args, currIndex, currIndex + splitCompatClassNamesLength)));
currIndex += splitCompatClassNamesLength;
// Load all jars that are on the classpath for the input jar for analyzing class hierarchy.
sFullClassPathJarPaths = new HashSet<>();
sFullClassPathJarPaths.clear();
sFullClassPathJarPaths.add(inputJarPath);
sFullClassPathJarPaths.addAll(sdkJarPaths);
sFullClassPathJarPaths.addAll(
Arrays.asList(Arrays.copyOfRange(args, currIndex, args.length)));
// Write list of references from Java class constant pools to specified output file
// sGenerateClassDepsPath. This is needed for keep rule generation for async DFMs.
if (!sGenerateClassDepsPath.isEmpty()) {
ConstantPoolReferenceReader.writeConstantPoolRefsToFile(
sFullClassPathJarPaths, sGenerateClassDepsPath);
}
sFullClassPathClassLoader = loadJars(sFullClassPathJarPaths);
sFullClassPathJarPaths.removeAll(directClassPathJarPaths);
sValidator = new ClassPathValidator();
process(inputJarPath, outputJarPath);
}
}