/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dx.command.dexer; import static java.lang.System.err; import static java.lang.System.out; import com.android.annotations.NonNull; import com.android.dex.Dex; import com.android.dex.DexException; import com.android.dex.DexFormat; import com.android.dex.util.FileUtils; import com.android.dx.Version; import com.android.dx.cf.code.SimException; import com.android.dx.cf.direct.ClassPathOpener; import com.android.dx.cf.direct.ClassPathOpener.FileNameFilter; import com.android.dx.cf.direct.DirectClassFile; import com.android.dx.cf.direct.StdAttributeFactory; import com.android.dx.cf.iface.ParseException; import com.android.dx.command.UsageException; import com.android.dx.dex.DexOptions; import com.android.dx.dex.cf.CfOptions; import com.android.dx.dex.cf.CfTranslator; import com.android.dx.dex.code.PositionList; import com.android.dx.dex.file.ClassDefItem; import com.android.dx.dex.file.DexFile; import com.android.dx.dex.file.EncodedMethod; import com.android.dx.dex.file.FieldIdItem; import com.android.dx.dex.file.Item; import com.android.dx.merge.CollisionPolicy; import com.android.dx.merge.DexMerger; import com.android.dx.rop.annotation.Annotation; import com.android.dx.rop.annotation.Annotations; import com.android.dx.rop.annotation.AnnotationsList; import com.android.dx.rop.code.RegisterSpec; import com.android.dx.rop.cst.CstNat; import com.android.dx.rop.cst.CstString; import com.android.dx.rop.cst.CstType; import com.android.dx.rop.type.Prototype; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; /** * Main class for the class file translator. */ public class Main { /** * Concurrency level used by intern tables. * * <p>We pick a value that seems to give good results on a typical laptop. */ public static final int CONCURRENCY_LEVEL = 8; /** * File extension of a {@code .dex} file. */ private static final String DEX_EXTENSION = ".dex"; /** * File name prefix of a {@code .dex} file automatically loaded in an * archive. */ private static final String DEX_PREFIX = "classes"; /** * {@code non-null;} the lengthy message that tries to discourage * people from defining core classes in applications */ private static final String IN_RE_CORE_CLASSES = "Ill-advised or mistaken usage of a core class (java.* or javax.*)\n" + "when not building a core library.\n\n" + "This is often due to inadvertently including a core library file\n" + "in your application's project, when using an IDE (such as\n" + "Eclipse). If you are sure you're not intentionally defining a\n" + "core class, then this is the most likely explanation of what's\n" + "going on.\n\n" + "However, you might actually be trying to define a class in a core\n" + "namespace, the source of which you may have taken, for example,\n" + "from a non-Android virtual machine project. This will most\n" + "assuredly not work. At a minimum, it jeopardizes the\n" + "compatibility of your app with future versions of the platform.\n" + "It is also often of questionable legality.\n\n" + "If you really intend to build a core library -- which is only\n" + "appropriate as part of creating a full virtual machine\n" + "distribution, as opposed to compiling an application -- then use\n" + "the \"--core-library\" option to suppress this error message.\n\n" + "If you go ahead and use \"--core-library\" but are in fact\n" + "building an application, then be forewarned that your application\n" + "will still fail to build or run, at some point. Please be\n" + "prepared for angry customers who find, for example, that your\n" + "application ceases to function once they upgrade their operating\n" + "system. You will be to blame for this problem.\n\n" + "If you are legitimately using some code that happens to be in a\n" + "core package, then the easiest safe alternative you have is to\n" + "repackage that code. That is, move the classes in question into\n" + "your own package namespace. This means that they will never be in\n" + "conflict with core system classes. JarJar is a tool that may help\n" + "you in this endeavor. If you find that you cannot do this, then\n" + "that is an indication that the path you are on will ultimately\n" + "lead to pain, suffering, grief, and lamentation.\n"; /** * {@code non-null;} name of the standard manifest file in {@code .jar} * files */ private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; /** * {@code non-null;} attribute name for the (quasi-standard?) * {@code Created-By} attribute */ private static final Attributes.Name CREATED_BY = new Attributes.Name("Created-By"); /** * {@code non-null;} list of {@code javax} subpackages that are considered * to be "core". <b>Note:</b>: This list must be sorted, since it * is binary-searched. */ private static final String[] JAVAX_CORE = { "accessibility", "crypto", "imageio", "management", "naming", "net", "print", "rmi", "security", "sip", "sound", "sql", "swing", "transaction", "xml" }; /* Array.newInstance may be added by RopperMachine, * ArrayIndexOutOfBoundsException.<init> may be added by EscapeAnalysis */ private static final int MAX_METHOD_ADDED_DURING_DEX_CREATION = 2; /* <primitive types box class>.TYPE */ private static final int MAX_FIELD_ADDED_DURING_DEX_CREATION = 9; /** number of errors during processing */ private AtomicInteger errors = new AtomicInteger(0); /** {@code non-null;} parsed command-line arguments */ private Arguments args; /** {@code non-null;} output file in-progress */ private DexFile outputDex; /** * {@code null-ok;} map of resources to include in the output, or * {@code null} if resources are being ignored */ private TreeMap<String, byte[]> outputResources; /** Library .dex files to merge into the output .dex. */ private final List<byte[]> libraryDexBuffers = new ArrayList<byte[]>(); /** Thread pool object used for multi-thread class translation. */ private ExecutorService classTranslatorPool; /** Single thread executor, for collecting results of parallel translation, * and adding classes to dex file in original input file order. */ private ExecutorService classDefItemConsumer; /** Futures for {@code classDefItemConsumer} tasks. */ private List<Future<Boolean>> addToDexFutures = new ArrayList<Future<Boolean>>(); /** Thread pool object used for multi-thread dex conversion (to byte array). * Used in combination with multi-dex support, to allow outputing * a completed dex file, in parallel with continuing processing. */ private ExecutorService dexOutPool; /** Futures for {@code dexOutPool} task. */ private List<Future<byte[]>> dexOutputFutures = new ArrayList<Future<byte[]>>(); /** Lock object used to to coordinate dex file rotation, and * multi-threaded translation. */ private Object dexRotationLock = new Object(); /** Record the number if method indices "reserved" for files * committed to translation in the context of the current dex * file, but not yet added. */ private int maxMethodIdsInProcess = 0; /** Record the number if field indices "reserved" for files * committed to translation in the context of the current dex * file, but not yet added. */ private int maxFieldIdsInProcess = 0; /** true if any files are successfully processed */ private volatile boolean anyFilesProcessed; /** class files older than this must be defined in the target dex file. */ private long minimumFileAge = 0; private Set<String> classesInMainDex = null; private List<byte[]> dexOutputArrays = new ArrayList<byte[]>(); private OutputStreamWriter humanOutWriter = null; private final DxContext context; public Main(DxContext context) { this.context = context; } /** * Run and exit if something unexpected happened. * @param argArray the command line arguments */ public static void main(String[] argArray) throws IOException { DxContext context = new DxContext(); Arguments arguments = new Arguments(); arguments.parseCommandLine(argArray, context); int result = new Main(context).run(arguments); if (result != 0) { System.exit(result); } } public static void clearInternTables() { Prototype.clearInternTable(); RegisterSpec.clearInternTable(); } /** * Run and return a result code. * @param arguments the data + parameters for the conversion * @return 0 if success > 0 otherwise. */ public int run(Arguments arguments) throws IOException { // Reset the error count to start fresh. errors.set(0); // empty the list, so that tools that load dx and keep it around // for multiple runs don't reuse older buffers. libraryDexBuffers.clear(); args = arguments; args.makeOptionsObjects(context); OutputStream humanOutRaw = null; if (args.humanOutName != null) { humanOutRaw = openOutput(args.humanOutName); humanOutWriter = new OutputStreamWriter(humanOutRaw); } try { if (args.multiDex) { return runMultiDex(); } else { return runMonoDex(); } } finally { closeOutput(humanOutRaw); } } /** * {@code non-null;} Error message for too many method/field/type ids. */ public static String getTooManyIdsErrorMessage() { return "Too many classes to fit in one dex file."; } private int runMonoDex() throws IOException { File incrementalOutFile = null; if (args.incremental) { if (args.outName == null) { err.println( "error: no incremental output name specified"); return -1; } incrementalOutFile = new File(args.outName); if (incrementalOutFile.exists()) { minimumFileAge = incrementalOutFile.lastModified(); } } if (!processAllFiles()) { return 1; } if (args.incremental && !anyFilesProcessed) { return 0; // this was a no-op incremental build } // this array is null if no classes were defined byte[] outArray = null; if (!outputDex.isEmpty() || (args.humanOutName != null)) { outArray = writeDex(outputDex); if (outArray == null) { return 2; } computeReferencedResources(); } if (args.incremental) { outArray = mergeIncremental(outArray, incrementalOutFile); } outArray = mergeLibraryDexBuffers(outArray); if (args.jarOutput) { // Effectively free up the (often massive) DexFile memory. outputDex = null; if (outArray != null) { outputResources.put(DexFormat.DEX_IN_JAR_NAME, outArray); } if (!createJar(args.outName)) { return 3; } } else if (outArray != null && args.outName != null) { OutputStream out = openOutput(args.outName); out.write(outArray); closeOutput(out); } return 0; } private int runMultiDex() throws IOException { assert !args.incremental; if (args.mainDexListFile != null) { classesInMainDex = new HashSet<String>(); readPathsFromFile(args.mainDexListFile, classesInMainDex); } dexOutPool = Executors.newFixedThreadPool(args.numThreads); if (!processAllFiles()) { return 1; } if (!libraryDexBuffers.isEmpty()) { throw new DexException("Library dex files are not supported in multi-dex mode"); } if (outputDex != null) { // this array is null if no classes were defined dexOutputFutures.add(dexOutPool.submit(new DexWriter(outputDex))); // Effectively free up the (often massive) DexFile memory. outputDex = null; } try { dexOutPool.shutdown(); if (!dexOutPool.awaitTermination(600L, TimeUnit.SECONDS)) { throw new RuntimeException("Timed out waiting for dex writer threads."); } for (Future<byte[]> f : dexOutputFutures) { dexOutputArrays.add(f.get()); } } catch (InterruptedException ex) { dexOutPool.shutdownNow(); throw new RuntimeException("A dex writer thread has been interrupted."); } catch (Exception e) { dexOutPool.shutdownNow(); throw new RuntimeException("Unexpected exception in dex writer thread"); } if (args.jarOutput) { for (int i = 0; i < dexOutputArrays.size(); i++) { outputResources.put(getDexFileName(i), dexOutputArrays.get(i)); } if (!createJar(args.outName)) { return 3; } } else if (args.outName != null) { File outDir = new File(args.outName); assert outDir.isDirectory(); for (int i = 0; i < dexOutputArrays.size(); i++) { OutputStream out = new FileOutputStream(new File(outDir, getDexFileName(i))); try { out.write(dexOutputArrays.get(i)); } finally { closeOutput(out); } } } return 0; } private static String getDexFileName(int i) { if (i == 0) { return DexFormat.DEX_IN_JAR_NAME; } else { return DEX_PREFIX + (i + 1) + DEX_EXTENSION; } } private static void readPathsFromFile(String fileName, Collection<String> paths) throws IOException { BufferedReader bfr = null; try { FileReader fr = new FileReader(fileName); bfr = new BufferedReader(fr); String line; while (null != (line = bfr.readLine())) { paths.add(fixPath(line)); } } finally { if (bfr != null) { bfr.close(); } } } /** * Merges the dex files {@code update} and {@code base}, preferring * {@code update}'s definition for types defined in both dex files. * * @param base a file to find the previous dex file. May be a .dex file, a * jar file possibly containing a .dex file, or null. * @return the bytes of the merged dex file, or null if both the update * and the base dex do not exist. */ private byte[] mergeIncremental(byte[] update, File base) throws IOException { Dex dexA = null; Dex dexB = null; if (update != null) { dexA = new Dex(update); } if (base.exists()) { dexB = new Dex(base); } Dex result; if (dexA == null && dexB == null) { return null; } else if (dexA == null) { result = dexB; } else if (dexB == null) { result = dexA; } else { DexMerger dexMerger = new DexMerger( new Dex[]{dexA, dexB}, CollisionPolicy.KEEP_FIRST, context); result = dexMerger.merge(); } ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); result.writeTo(bytesOut); return bytesOut.toByteArray(); } /** * Merges the dex files in library jars. If multiple dex files define the * same type, this fails with an exception. */ private byte[] mergeLibraryDexBuffers(byte[] outArray) throws IOException { ArrayList<Dex> dexes = new ArrayList<Dex>(); if (outArray != null) { dexes.add(new Dex(outArray)); } for (byte[] libraryDex : libraryDexBuffers) { dexes.add(new Dex(libraryDex)); } if (dexes.isEmpty()) { return null; } DexMerger dexMerger = new DexMerger( dexes.toArray(new Dex[dexes.size()]), CollisionPolicy.FAIL, context); Dex merged = dexMerger.merge(); return merged.getBytes(); } /** * Constructs the output {@link DexFile}, fill it in with all the * specified classes, and populate the resources map if required. * * @return whether processing was successful */ private boolean processAllFiles() { createDexFile(); if (args.jarOutput) { outputResources = new TreeMap<String, byte[]>(); } anyFilesProcessed = false; String[] fileNames = args.fileNames; Arrays.sort(fileNames); // translate classes in parallel classTranslatorPool = new ThreadPoolExecutor(args.numThreads, args.numThreads, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2 * args.numThreads, true), new ThreadPoolExecutor.CallerRunsPolicy()); // collect translated and write to dex in order classDefItemConsumer = Executors.newSingleThreadExecutor(); try { if (args.mainDexListFile != null) { // with --main-dex-list FileNameFilter mainPassFilter = args.strictNameCheck ? new MainDexListFilter() : new BestEffortMainDexListFilter(); // forced in main dex for (int i = 0; i < fileNames.length; i++) { processOne(fileNames[i], mainPassFilter); } if (dexOutputFutures.size() > 0) { throw new DexException("Too many classes in " + Arguments.MAIN_DEX_LIST_OPTION + ", main dex capacity exceeded"); } if (args.minimalMainDex) { // start second pass directly in a secondary dex file. // Wait for classes in progress to complete synchronized(dexRotationLock) { while(maxMethodIdsInProcess > 0 || maxFieldIdsInProcess > 0) { try { dexRotationLock.wait(); } catch(InterruptedException ex) { /* ignore */ } } } rotateDexFile(); } // remaining files for (int i = 0; i < fileNames.length; i++) { processOne(fileNames[i], new NotFilter(mainPassFilter)); } } else { // without --main-dex-list for (int i = 0; i < fileNames.length; i++) { processOne(fileNames[i], ClassPathOpener.acceptAll); } } } catch (StopProcessing ex) { /* * Ignore it and just let the error reporting do * their things. */ } try { classTranslatorPool.shutdown(); classTranslatorPool.awaitTermination(600L, TimeUnit.SECONDS); classDefItemConsumer.shutdown(); classDefItemConsumer.awaitTermination(600L, TimeUnit.SECONDS); for (Future<Boolean> f : addToDexFutures) { try { f.get(); } catch(ExecutionException ex) { // Catch any previously uncaught exceptions from // class translation and adding to dex. int count = errors.incrementAndGet(); if (count < 10) { if (args.debug) { context.err.println("Uncaught translation error:"); ex.getCause().printStackTrace(context.err); } else { context.err.println("Uncaught translation error: " + ex.getCause()); } } else { throw new InterruptedException("Too many errors"); } } } } catch (InterruptedException ie) { classTranslatorPool.shutdownNow(); classDefItemConsumer.shutdownNow(); throw new RuntimeException("Translation has been interrupted", ie); } catch (Exception e) { classTranslatorPool.shutdownNow(); classDefItemConsumer.shutdownNow(); e.printStackTrace(out); throw new RuntimeException("Unexpected exception in translator thread.", e); } int errorNum = errors.get(); if (errorNum != 0) { context.err.println(errorNum + " error" + ((errorNum == 1) ? "" : "s") + "; aborting"); return false; } if (args.incremental && !anyFilesProcessed) { return true; } if (!(anyFilesProcessed || args.emptyOk)) { context.err.println("no classfiles specified"); return false; } if (args.optimize && args.statistics) { context.codeStatistics.dumpStatistics(context.out); } return true; } private void createDexFile() { outputDex = new DexFile(args.dexOptions); if (args.dumpWidth != 0) { outputDex.setDumpWidth(args.dumpWidth); } } private void rotateDexFile() { if (outputDex != null) { if (dexOutPool != null) { dexOutputFutures.add(dexOutPool.submit(new DexWriter(outputDex))); } else { dexOutputArrays.add(writeDex(outputDex)); } } createDexFile(); } /** * Processes one pathname element. * * @param pathname {@code non-null;} the pathname to process. May * be the path of a class file, a jar file, or a directory * containing class files. * @param filter {@code non-null;} A filter for excluding files. */ private void processOne(String pathname, FileNameFilter filter) { ClassPathOpener opener; opener = new ClassPathOpener(pathname, true, filter, new FileBytesConsumer()); if (opener.process()) { updateStatus(true); } } private void updateStatus(boolean res) { anyFilesProcessed |= res; } /** * Processes one file, which may be either a class or a resource. * * @param name {@code non-null;} name of the file * @param bytes {@code non-null;} contents of the file * @return whether processing was successful */ private boolean processFileBytes(String name, long lastModified, byte[] bytes) { boolean isClass = name.endsWith(".class"); boolean isClassesDex = name.equals(DexFormat.DEX_IN_JAR_NAME); boolean keepResources = (outputResources != null); if (!isClass && !isClassesDex && !keepResources) { if (args.verbose) { context.out.println("ignored resource " + name); } return false; } if (args.verbose) { context.out.println("processing " + name + "..."); } String fixedName = fixPath(name); if (isClass) { if (keepResources && args.keepClassesInJar) { synchronized (outputResources) { outputResources.put(fixedName, bytes); } } if (lastModified < minimumFileAge) { return true; } processClass(fixedName, bytes); // Assume that an exception may occur. Status will be updated // asynchronously, if the class compiles without error. return false; } else if (isClassesDex) { synchronized (libraryDexBuffers) { libraryDexBuffers.add(bytes); } return true; } else { synchronized (outputResources) { outputResources.put(fixedName, bytes); } return true; } } /** * Processes one classfile. * * @param name {@code non-null;} name of the file, clipped such that it * <i>should</i> correspond to the name of the class it contains * @param bytes {@code non-null;} contents of the file * @return whether processing was successful */ private boolean processClass(String name, byte[] bytes) { if (! args.coreLibrary) { checkClassName(name); } try { new DirectClassFileConsumer(name, bytes, null).call( new ClassParserTask(name, bytes).call()); } catch(Exception ex) { throw new RuntimeException("Exception parsing classes", ex); } return true; } private DirectClassFile parseClass(String name, byte[] bytes) { DirectClassFile cf = new DirectClassFile(bytes, name, args.cfOptions.strictNameCheck); cf.setAttributeFactory(StdAttributeFactory.THE_ONE); cf.getMagic(); // triggers the actual parsing return cf; } private ClassDefItem translateClass(byte[] bytes, DirectClassFile cf) { try { return CfTranslator.translate(context, cf, bytes, args.cfOptions, args.dexOptions, outputDex); } catch (ParseException ex) { context.err.println("\ntrouble processing:"); if (args.debug) { ex.printStackTrace(context.err); } else { ex.printContext(context.err); } } errors.incrementAndGet(); return null; } private boolean addClassToDex(ClassDefItem clazz) { synchronized (outputDex) { outputDex.add(clazz); } return true; } /** * Check the class name to make sure it's not a "core library" * class. If there is a problem, this updates the error count and * throws an exception to stop processing. * * @param name {@code non-null;} the fully-qualified internal-form * class name */ private void checkClassName(String name) { boolean bogus = false; if (name.startsWith("java/")) { bogus = true; } else if (name.startsWith("javax/")) { int slashAt = name.indexOf('/', 6); if (slashAt == -1) { // Top-level javax classes are verboten. bogus = true; } else { String pkg = name.substring(6, slashAt); bogus = (Arrays.binarySearch(JAVAX_CORE, pkg) >= 0); } } if (! bogus) { return; } /* * The user is probably trying to include an entire desktop * core library in a misguided attempt to get their application * working. Try to help them understand what's happening. */ context.err.println("\ntrouble processing \"" + name + "\":\n\n" + IN_RE_CORE_CLASSES); errors.incrementAndGet(); throw new StopProcessing(); } /** * Converts {@link #outputDex} into a {@code byte[]} and do whatever * human-oriented dumping is required. * * @return {@code null-ok;} the converted {@code byte[]} or {@code null} * if there was a problem */ private byte[] writeDex(DexFile outputDex) { byte[] outArray = null; try { try { if (args.methodToDump != null) { /* * Simply dump the requested method. Note: The call * to toDex() is required just to get the underlying * structures ready. */ outputDex.toDex(null, false); dumpMethod(outputDex, args.methodToDump, humanOutWriter); } else { /* * This is the usual case: Create an output .dex file, * and write it, dump it, etc. */ outArray = outputDex.toDex(humanOutWriter, args.verboseDump); } if (args.statistics) { context.out.println(outputDex.getStatistics().toHuman()); } } finally { if (humanOutWriter != null) { humanOutWriter.flush(); } } } catch (Exception ex) { if (args.debug) { context.err.println("\ntrouble writing output:"); ex.printStackTrace(context.err); } else { context.err.println("\ntrouble writing output: " + ex.getMessage()); } return null; } return outArray; } /** * Creates a jar file from the resources (including dex file arrays). * * @param fileName {@code non-null;} name of the file * @return whether the creation was successful */ private boolean createJar(String fileName) { /* * Make or modify the manifest (as appropriate), put the dex * array into the resources map, and then process the entire * resources map in a uniform manner. */ try { Manifest manifest = makeManifest(); OutputStream out = openOutput(fileName); JarOutputStream jarOut = new JarOutputStream(out, manifest); try { for (Map.Entry<String, byte[]> e : outputResources.entrySet()) { String name = e.getKey(); byte[] contents = e.getValue(); JarEntry entry = new JarEntry(name); int length = contents.length; if (args.verbose) { context.out.println("writing " + name + "; size " + length + "..."); } entry.setSize(length); jarOut.putNextEntry(entry); jarOut.write(contents); jarOut.closeEntry(); } } finally { jarOut.finish(); jarOut.flush(); closeOutput(out); } } catch (Exception ex) { if (args.debug) { context.err.println("\ntrouble writing output:"); ex.printStackTrace(context.err); } else { context.err.println("\ntrouble writing output: " + ex.getMessage()); } return false; } return true; } /** * Creates and returns the manifest to use for the output. This may * modify {@link #outputResources} (removing the pre-existing manifest). * * @return {@code non-null;} the manifest */ private Manifest makeManifest() throws IOException { byte[] manifestBytes = outputResources.get(MANIFEST_NAME); Manifest manifest; Attributes attribs; if (manifestBytes == null) { // We need to construct an entirely new manifest. manifest = new Manifest(); attribs = manifest.getMainAttributes(); attribs.put(Attributes.Name.MANIFEST_VERSION, "1.0"); } else { manifest = new Manifest(new ByteArrayInputStream(manifestBytes)); attribs = manifest.getMainAttributes(); outputResources.remove(MANIFEST_NAME); } String createdBy = attribs.getValue(CREATED_BY); if (createdBy == null) { createdBy = ""; } else { createdBy += " + "; } createdBy += "dx " + Version.VERSION; attribs.put(CREATED_BY, createdBy); attribs.putValue("Dex-Location", DexFormat.DEX_IN_JAR_NAME); return manifest; } /** * Opens and returns the named file for writing, treating "-" specially. * * @param name {@code non-null;} the file name * @return {@code non-null;} the opened file */ private static OutputStream openOutput(String name) throws IOException { if (name.equals("-") || name.startsWith("-.")) { return out; } return new FileOutputStream(name); } /** * Flushes and closes the given output stream, except if it happens to be * {@link System#out} in which case this method does the flush but not * the close. This method will also silently do nothing if given a * {@code null} argument. * * @param stream {@code null-ok;} what to close */ private static void closeOutput(OutputStream stream) throws IOException { if (stream == null) { return; } stream.flush(); if (stream != out) { stream.close(); } } /** * Returns the "fixed" version of a given file path, suitable for * use as a path within a {@code .jar} file and for checking * against a classfile-internal "this class" name. This looks for * the last instance of the substring {@code "/./"} within * the path, and if it finds it, it takes the portion after to be * the fixed path. If that isn't found but the path starts with * {@code "./"}, then that prefix is removed and the rest is * return. If neither of these is the case, this method returns * its argument. * * @param path {@code non-null;} the path to "fix" * @return {@code non-null;} the fixed version (which might be the same as * the given {@code path}) */ private static String fixPath(String path) { /* * If the path separator is \ (like on windows), we convert the * path to a standard '/' separated path. */ if (File.separatorChar == '\\') { path = path.replace('\\', '/'); } int index = path.lastIndexOf("/./"); if (index != -1) { return path.substring(index + 3); } if (path.startsWith("./")) { return path.substring(2); } return path; } /** * Dumps any method with the given name in the given file. * * @param dex {@code non-null;} the dex file * @param fqName {@code non-null;} the fully-qualified name of the * method(s) * @param out {@code non-null;} where to dump to */ private void dumpMethod(DexFile dex, String fqName, OutputStreamWriter out) { boolean wildcard = fqName.endsWith("*"); int lastDot = fqName.lastIndexOf('.'); if ((lastDot <= 0) || (lastDot == (fqName.length() - 1))) { context.err.println("bogus fully-qualified method name: " + fqName); return; } String className = fqName.substring(0, lastDot).replace('.', '/'); String methodName = fqName.substring(lastDot + 1); ClassDefItem clazz = dex.getClassOrNull(className); if (clazz == null) { context.err.println("no such class: " + className); return; } if (wildcard) { methodName = methodName.substring(0, methodName.length() - 1); } ArrayList<EncodedMethod> allMeths = clazz.getMethods(); TreeMap<CstNat, EncodedMethod> meths = new TreeMap<CstNat, EncodedMethod>(); /* * Figure out which methods to include in the output, and get them * all sorted, so that the printout code is robust with respect to * changes in the underlying order. */ for (EncodedMethod meth : allMeths) { String methName = meth.getName().getString(); if ((wildcard && methName.startsWith(methodName)) || (!wildcard && methName.equals(methodName))) { meths.put(meth.getRef().getNat(), meth); } } if (meths.size() == 0) { context.err.println("no such method: " + fqName); return; } PrintWriter pw = new PrintWriter(out); for (EncodedMethod meth : meths.values()) { // TODO: Better stuff goes here, perhaps. meth.debugPrint(pw, args.verboseDump); /* * The (default) source file is an attribute of the class, but * it's useful to see it in method dumps. */ CstString sourceFile = clazz.getSourceFile(); if (sourceFile != null) { pw.println(" source file: " + sourceFile.toQuoted()); } Annotations methodAnnotations = clazz.getMethodAnnotations(meth.getRef()); AnnotationsList parameterAnnotations = clazz.getParameterAnnotations(meth.getRef()); if (methodAnnotations != null) { pw.println(" method annotations:"); for (Annotation a : methodAnnotations.getAnnotations()) { pw.println(" " + a); } } if (parameterAnnotations != null) { pw.println(" parameter annotations:"); int sz = parameterAnnotations.size(); for (int i = 0; i < sz; i++) { pw.println(" parameter " + i); Annotations annotations = parameterAnnotations.get(i); for (Annotation a : annotations.getAnnotations()) { pw.println(" " + a); } } } } pw.flush(); } private static class NotFilter implements FileNameFilter { private final FileNameFilter filter; private NotFilter(FileNameFilter filter) { this.filter = filter; } @Override public boolean accept(String path) { return !filter.accept(path); } } /** * A quick and accurate filter for when file path can be trusted. */ private class MainDexListFilter implements FileNameFilter { @Override public boolean accept(String fullPath) { if (fullPath.endsWith(".class")) { String path = fixPath(fullPath); return classesInMainDex.contains(path); } else { return true; } } } /** * A best effort conservative filter for when file path can <b>not</b> be trusted. */ private class BestEffortMainDexListFilter implements FileNameFilter { Map<String, List<String>> map = new HashMap<String, List<String>>(); public BestEffortMainDexListFilter() { for (String pathOfClass : classesInMainDex) { String normalized = fixPath(pathOfClass); String simple = getSimpleName(normalized); List<String> fullPath = map.get(simple); if (fullPath == null) { fullPath = new ArrayList<String>(1); map.put(simple, fullPath); } fullPath.add(normalized); } } @Override public boolean accept(String path) { if (path.endsWith(".class")) { String normalized = fixPath(path); String simple = getSimpleName(normalized); List<String> fullPaths = map.get(simple); if (fullPaths != null) { for (String fullPath : fullPaths) { if (normalized.endsWith(fullPath)) { return true; } } } return false; } else { return true; } } private String getSimpleName(String path) { int index = path.lastIndexOf('/'); if (index >= 0) { return path.substring(index + 1); } else { return path; } } } /** * Exception class used to halt processing prematurely. */ private static class StopProcessing extends RuntimeException { // This space intentionally left blank. } /** * Command-line argument parser and access. */ public static class Arguments { private static final String MINIMAL_MAIN_DEX_OPTION = "--minimal-main-dex"; private static final String MAIN_DEX_LIST_OPTION = "--main-dex-list"; private static final String MULTI_DEX_OPTION = "--multi-dex"; private static final String NUM_THREADS_OPTION = "--num-threads"; private static final String INCREMENTAL_OPTION = "--incremental"; private static final String INPUT_LIST_OPTION = "--input-list"; /** whether to run in debug mode */ public boolean debug = false; /** whether to emit warning messages */ public boolean warnings = true; /** whether to emit high-level verbose human-oriented output */ public boolean verbose = false; /** whether to emit verbose human-oriented output in the dump file */ public boolean verboseDump = false; /** whether we are constructing a core library */ public boolean coreLibrary = false; /** {@code null-ok;} particular method to dump */ public String methodToDump = null; /** max width for columnar output */ public int dumpWidth = 0; /** {@code null-ok;} output file name for binary file */ public String outName = null; /** {@code null-ok;} output file name for human-oriented dump */ public String humanOutName = null; /** whether strict file-name-vs-class-name checking should be done */ public boolean strictNameCheck = true; /** * whether it is okay for there to be no {@code .class} files * to process */ public boolean emptyOk = false; /** * whether the binary output is to be a {@code .jar} file * instead of a plain {@code .dex} */ public boolean jarOutput = false; /** * when writing a {@code .jar} file, whether to still * keep the {@code .class} files */ public boolean keepClassesInJar = false; /** how much source position info to preserve */ public int positionInfo = PositionList.LINES; /** whether to keep local variable information */ public boolean localInfo = true; /** whether to merge with the output dex file if it exists. */ public boolean incremental = false; /** whether to force generation of const-string/jumbo for all indexes, * to allow merges between dex files with many strings. */ public boolean forceJumbo = false; /** {@code non-null} after {@link #parseCommandLine}; file name arguments */ public String[] fileNames; /** whether to do SSA/register optimization */ public boolean optimize = true; /** Filename containg list of methods to optimize */ public String optimizeListFile = null; /** Filename containing list of methods to NOT optimize */ public String dontOptimizeListFile = null; /** Whether to print statistics to stdout at end of compile cycle */ public boolean statistics; /** Options for class file transformation */ public CfOptions cfOptions; /** Options for dex file output */ public DexOptions dexOptions; /** number of threads to run with */ public int numThreads = 1; /** generation of multiple dex is allowed */ public boolean multiDex = false; /** Optional file containing a list of class files containing classes to be forced in main * dex */ public String mainDexListFile = null; /** Produce the smallest possible main dex. Ignored unless multiDex is true and * mainDexListFile is specified and non empty. */ public boolean minimalMainDex = false; /** Optional list containing inputs read in from a file. */ private List<String> inputList = null; private int maxNumberOfIdxPerDex = DexFormat.MAX_MEMBER_IDX + 1; private static class ArgumentsParser { /** The arguments to process. */ private final String[] arguments; /** The index of the next argument to process. */ private int index; /** The current argument being processed after a {@link #getNext()} call. */ private String current; /** The last value of an argument processed by {@link #isArg(String)}. */ private String lastValue; public ArgumentsParser(String[] arguments) { this.arguments = arguments; index = 0; } public String getCurrent() { return current; } public String getLastValue() { return lastValue; } /** * Moves on to the next argument. * Returns false when we ran out of arguments that start with --. */ public boolean getNext() { if (index >= arguments.length) { return false; } current = arguments[index]; if (current.equals("--") || !current.startsWith("--")) { return false; } index++; return true; } /** * Similar to {@link #getNext()}, this moves on the to next argument. * It does not check however whether the argument starts with -- * and thus can be used to retrieve values. */ private boolean getNextValue() { if (index >= arguments.length) { return false; } current = arguments[index]; index++; return true; } /** * Returns all the arguments that have not been processed yet. */ public String[] getRemaining() { int n = arguments.length - index; String[] remaining = new String[n]; if (n > 0) { System.arraycopy(arguments, index, remaining, 0, n); } return remaining; } /** * Checks the current argument against the given prefix. * If prefix is in the form '--name=', an extra value is expected. * The argument can then be in the form '--name=value' or as a 2-argument * form '--name value'. */ public boolean isArg(String prefix) { int n = prefix.length(); if (n > 0 && prefix.charAt(n-1) == '=') { // Argument accepts a value. Capture it. if (current.startsWith(prefix)) { // Argument is in the form --name=value, split the value out lastValue = current.substring(n); return true; } else { // Check whether we have "--name value" as 2 arguments prefix = prefix.substring(0, n-1); if (current.equals(prefix)) { if (getNextValue()) { lastValue = current; return true; } else { err.println("Missing value after parameter " + prefix); throw new UsageException(); } } return false; } } else { // Argument does not accept a value. return current.equals(prefix); } } } private static class OutputOptions { boolean outputIsDirectory = false; boolean outputIsDirectDex = false; } /** * Parses all command-line arguments. * * @param args {@code non-null;} the arguments * @param context */ public void parseCommandLine(String[] args, DxContext context) { ArgumentsParser parser = new ArgumentsParser(args); OutputOptions outputOptions = parseFlags(parser); fileNames = parser.getRemaining(); if(inputList != null && !inputList.isEmpty()) { // append the file names to the end of the input list inputList.addAll(Arrays.asList(fileNames)); fileNames = inputList.toArray(new String[inputList.size()]); } if (fileNames.length == 0) { if (!emptyOk) { err.println("no input files specified"); throw new UsageException(); } } else if (emptyOk) { out.println("ignoring input files"); } if ((humanOutName == null) && (methodToDump != null)) { humanOutName = "-"; } if (mainDexListFile != null && !multiDex) { err.println(MAIN_DEX_LIST_OPTION + " is only supported in combination with " + MULTI_DEX_OPTION); throw new UsageException(); } if (minimalMainDex && (mainDexListFile == null || !multiDex)) { err.println(MINIMAL_MAIN_DEX_OPTION + " is only supported in combination with " + MULTI_DEX_OPTION + " and " + MAIN_DEX_LIST_OPTION); throw new UsageException(); } if (multiDex && incremental) { err.println(INCREMENTAL_OPTION + " is not supported with " + MULTI_DEX_OPTION); throw new UsageException(); } if (multiDex && outputOptions.outputIsDirectDex) { err.println("Unsupported output \"" + outName +"\". " + MULTI_DEX_OPTION + " supports only archive or directory output"); throw new UsageException(); } if (outputOptions.outputIsDirectory && !multiDex) { outName = new File(outName, DexFormat.DEX_IN_JAR_NAME).getPath(); } makeOptionsObjects(context); } public void parseFlags(String[] flags) { parseFlags(new ArgumentsParser(flags)); } @NonNull private OutputOptions parseFlags(ArgumentsParser parser) { OutputOptions outputOptions = new OutputOptions(); while(parser.getNext()) { if (parser.isArg("--debug")) { debug = true; } else if (parser.isArg("--no-warning")) { warnings = false; } else if (parser.isArg("--verbose")) { verbose = true; } else if (parser.isArg("--verbose-dump")) { verboseDump = true; } else if (parser.isArg("--no-files")) { emptyOk = true; } else if (parser.isArg("--no-optimize")) { optimize = false; } else if (parser.isArg("--no-strict")) { strictNameCheck = false; } else if (parser.isArg("--core-library")) { coreLibrary = true; } else if (parser.isArg("--statistics")) { statistics = true; } else if (parser.isArg("--optimize-list=")) { if (dontOptimizeListFile != null) { err.println("--optimize-list and " + "--no-optimize-list are incompatible."); throw new UsageException(); } optimize = true; optimizeListFile = parser.getLastValue(); } else if (parser.isArg("--no-optimize-list=")) { if (dontOptimizeListFile != null) { err.println("--optimize-list and " + "--no-optimize-list are incompatible."); throw new UsageException(); } optimize = true; dontOptimizeListFile = parser.getLastValue(); } else if (parser.isArg("--keep-classes")) { keepClassesInJar = true; } else if (parser.isArg("--output=")) { outName = parser.getLastValue(); if (new File(outName).isDirectory()) { jarOutput = false; outputOptions.outputIsDirectory = true; } else if (FileUtils.hasArchiveSuffix(outName)) { jarOutput = true; } else if (outName.endsWith(".dex") || outName.equals("-")) { jarOutput = false; outputOptions.outputIsDirectDex = true; } else { err.println("unknown output extension: " + outName); throw new UsageException(); } } else if (parser.isArg("--dump-to=")) { humanOutName = parser.getLastValue(); } else if (parser.isArg("--dump-width=")) { dumpWidth = Integer.parseInt(parser.getLastValue()); } else if (parser.isArg("--dump-method=")) { methodToDump = parser.getLastValue(); jarOutput = false; } else if (parser.isArg("--positions=")) { String pstr = parser.getLastValue().intern(); if (pstr == "none") { positionInfo = PositionList.NONE; } else if (pstr == "important") { positionInfo = PositionList.IMPORTANT; } else if (pstr == "lines") { positionInfo = PositionList.LINES; } else { err.println("unknown positions option: " + pstr); throw new UsageException(); } } else if (parser.isArg("--no-locals")) { localInfo = false; } else if (parser.isArg(NUM_THREADS_OPTION + "=")) { numThreads = Integer.parseInt(parser.getLastValue()); } else if (parser.isArg(INCREMENTAL_OPTION)) { incremental = true; } else if (parser.isArg("--force-jumbo")) { forceJumbo = true; } else if (parser.isArg(MULTI_DEX_OPTION)) { multiDex = true; } else if (parser.isArg(MAIN_DEX_LIST_OPTION + "=")) { mainDexListFile = parser.getLastValue(); } else if (parser.isArg(MINIMAL_MAIN_DEX_OPTION)) { minimalMainDex = true; } else if (parser.isArg("--set-max-idx-number=")) { // undocumented test option maxNumberOfIdxPerDex = Integer.parseInt(parser.getLastValue()); } else if(parser.isArg(INPUT_LIST_OPTION + "=")) { File inputListFile = new File(parser.getLastValue()); try{ inputList = new ArrayList<String>(); readPathsFromFile(inputListFile.getAbsolutePath(), inputList); } catch(IOException e) { err.println( "Unable to read input list file: " + inputListFile.getName()); // problem reading the file so we should halt execution throw new UsageException(); } } else { err.println("unknown option: " + parser.getCurrent()); throw new UsageException(); } } return outputOptions; } /** * Copies relevant arguments over into CfOptions and * DexOptions instances. */ public void makeOptionsObjects(DxContext context) { cfOptions = new CfOptions(); cfOptions.positionInfo = positionInfo; cfOptions.localInfo = localInfo; cfOptions.strictNameCheck = strictNameCheck; cfOptions.optimize = optimize; cfOptions.optimizeListFile = optimizeListFile; cfOptions.dontOptimizeListFile = dontOptimizeListFile; cfOptions.statistics = statistics; if (warnings) { cfOptions.warn = context.err; } else { cfOptions.warn = context.noop; } dexOptions = new DexOptions(); dexOptions.forceJumbo = forceJumbo; } } /** * Callback class for processing input file bytes, produced by the * ClassPathOpener. */ private class FileBytesConsumer implements ClassPathOpener.Consumer { @Override public boolean processFileBytes(String name, long lastModified, byte[] bytes) { return Main.this.processFileBytes(name, lastModified, bytes); } @Override public void onException(Exception ex) { if (ex instanceof StopProcessing) { throw (StopProcessing) ex; } else if (ex instanceof SimException) { context.err.println("\nEXCEPTION FROM SIMULATION:"); context.err.println(ex.getMessage() + "\n"); context.err.println(((SimException) ex).getContext()); } else { context.err.println("\nUNEXPECTED TOP-LEVEL EXCEPTION:"); ex.printStackTrace(context.err); } errors.incrementAndGet(); } @Override public void onProcessArchiveStart(File file) { if (args.verbose) { context.out.println("processing archive " + file + "..."); } } } /** Callable helper class to parseCommandLine class bytes. */ private class ClassParserTask implements Callable<DirectClassFile> { String name; byte[] bytes; private ClassParserTask(String name, byte[] bytes) { this.name = name; this.bytes = bytes; } @Override public DirectClassFile call() throws Exception { DirectClassFile cf = parseClass(name, bytes); return cf; } } /** * Callable helper class used to sequentially collect the results of * the (optionally parallel) translation phase, in correct input file order. * This class is also responsible for coordinating dex file rotation * with the ClassDefItemConsumer class. * We maintain invariant that the number of indices used in the current * dex file plus the max number of indices required by classes passed to * the translation phase and not yet added to the dex file, is less than * or equal to the dex file limit. * For each parsed file, we estimate the maximum number of indices it may * require. If passing the file to the translation phase would invalidate * the invariant, we wait, until the next class is added to the dex file, * and then reevaluate the invariant. If there are no further classes in * the translation phase, we rotate the dex file. */ private class DirectClassFileConsumer implements Callable<Boolean> { String name; byte[] bytes; Future<DirectClassFile> dcff; private DirectClassFileConsumer(String name, byte[] bytes, Future<DirectClassFile> dcff) { this.name = name; this.bytes = bytes; this.dcff = dcff; } @Override public Boolean call() throws Exception { DirectClassFile cf = dcff.get(); return call(cf); } private Boolean call(DirectClassFile cf) { int maxMethodIdsInClass = 0; int maxFieldIdsInClass = 0; if (args.multiDex) { // Calculate max number of indices this class will add to the // dex file. // The possibility of overloading means that we can't easily // know how many constant are needed for declared methods and // fields. We therefore make the simplifying assumption that // all constants are external method or field references. int constantPoolSize = cf.getConstantPool().size(); maxMethodIdsInClass = constantPoolSize + cf.getMethods().size() + MAX_METHOD_ADDED_DURING_DEX_CREATION; maxFieldIdsInClass = constantPoolSize + cf.getFields().size() + MAX_FIELD_ADDED_DURING_DEX_CREATION; synchronized(dexRotationLock) { int numMethodIds; int numFieldIds; // Number of indices used in current dex file. synchronized(outputDex) { numMethodIds = outputDex.getMethodIds().items().size(); numFieldIds = outputDex.getFieldIds().items().size(); } // Wait until we're sure this class will fit in the current // dex file. while(((numMethodIds + maxMethodIdsInClass + maxMethodIdsInProcess > args.maxNumberOfIdxPerDex) || (numFieldIds + maxFieldIdsInClass + maxFieldIdsInProcess > args.maxNumberOfIdxPerDex))) { if (maxMethodIdsInProcess > 0 || maxFieldIdsInProcess > 0) { // There are classes in the translation phase that // have not yet been added to the dex file, so we // wait for the next class to complete. try { dexRotationLock.wait(); } catch(InterruptedException ex) { /* ignore */ } } else if (outputDex.getClassDefs().items().size() > 0) { // There are no further classes in the translation // phase, and we have a full dex file. Rotate! rotateDexFile(); } else { // The estimated number of indices is too large for // an empty dex file. We proceed hoping the actual // number of indices needed will fit. break; } synchronized(outputDex) { numMethodIds = outputDex.getMethodIds().items().size(); numFieldIds = outputDex.getFieldIds().items().size(); } } // Add our estimate to the total estimate for // classes under translation. maxMethodIdsInProcess += maxMethodIdsInClass; maxFieldIdsInProcess += maxFieldIdsInClass; } } // Submit class to translation phase. Future<ClassDefItem> cdif = classTranslatorPool.submit( new ClassTranslatorTask(name, bytes, cf)); Future<Boolean> res = classDefItemConsumer.submit(new ClassDefItemConsumer( name, cdif, maxMethodIdsInClass, maxFieldIdsInClass)); addToDexFutures.add(res); return true; } } /** Callable helper class to translate classes in parallel */ private class ClassTranslatorTask implements Callable<ClassDefItem> { String name; byte[] bytes; DirectClassFile classFile; private ClassTranslatorTask(String name, byte[] bytes, DirectClassFile classFile) { this.name = name; this.bytes = bytes; this.classFile = classFile; } @Override public ClassDefItem call() { ClassDefItem clazz = translateClass(bytes, classFile); return clazz; } } /** * Callable helper class used to collect the results of * the parallel translation phase, adding the translated classes to * the current dex file in correct (deterministic) file order. * This class is also responsible for coordinating dex file rotation * with the DirectClassFileConsumer class. */ private class ClassDefItemConsumer implements Callable<Boolean> { String name; Future<ClassDefItem> futureClazz; int maxMethodIdsInClass; int maxFieldIdsInClass; private ClassDefItemConsumer(String name, Future<ClassDefItem> futureClazz, int maxMethodIdsInClass, int maxFieldIdsInClass) { this.name = name; this.futureClazz = futureClazz; this.maxMethodIdsInClass = maxMethodIdsInClass; this.maxFieldIdsInClass = maxFieldIdsInClass; } @Override public Boolean call() throws Exception { try { ClassDefItem clazz = futureClazz.get(); if (clazz != null) { addClassToDex(clazz); updateStatus(true); } return true; } catch(ExecutionException ex) { // Rethrow previously uncaught translation exceptions. // These, as well as any exceptions from addClassToDex, // are handled and reported in processAllFiles(). Throwable t = ex.getCause(); throw (t instanceof Exception) ? (Exception) t : ex; } finally { if (args.multiDex) { // Having added our actual indicies to the dex file, // we subtract our original estimate from the total estimate, // and signal the translation phase, which may be paused // waiting to determine if more classes can be added to the // current dex file, or if a new dex file must be created. synchronized(dexRotationLock) { maxMethodIdsInProcess -= maxMethodIdsInClass; maxFieldIdsInProcess -= maxFieldIdsInClass; dexRotationLock.notifyAll(); } } } } } /** Callable helper class to convert dex files in worker threads */ private class DexWriter implements Callable<byte[]> { private DexFile dexFile; private DexWriter(DexFile dexFile) { this.dexFile = dexFile; } @Override public byte[] call() throws IOException { return writeDex(dexFile); } } // Facebook addition: compute the resources referenced by this dex file. // Does not apply to any merging, just the input class. private final Set<String> resourceNames = new HashSet<>(); public Collection<String> getReferencedResourceNames() { return resourceNames; } private void computeReferencedResources() { for (Item genericItem : outputDex.getFieldIds().items()) { FieldIdItem item = (FieldIdItem) genericItem; CstType fieldClass = item.getDefiningClass(); CstString fieldName = item.getRef().getNat().getName(); if (fieldClass.getClassType().getDescriptor().contains("/R$")) { // Add the packageName of the class for better accuracy. resourceNames.add(fieldClass.getPackageName() + "." + fieldName.getString()); } } } }