Home | History | Annotate | Download | only in dexer
      1 /*
      2  * Copyright (C) 2007 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 package com.android.dx.command.dexer;
     18 
     19 import com.android.dx.Version;
     20 import com.android.dx.cf.code.SimException;
     21 import com.android.dx.cf.direct.ClassPathOpener;
     22 import com.android.dx.cf.iface.ParseException;
     23 import com.android.dx.command.DxConsole;
     24 import com.android.dx.command.UsageException;
     25 import com.android.dx.dex.DexFormat;
     26 import com.android.dx.dex.DexOptions;
     27 import com.android.dx.dex.cf.CfOptions;
     28 import com.android.dx.dex.cf.CfTranslator;
     29 import com.android.dx.dex.cf.CodeStatistics;
     30 import com.android.dx.dex.code.PositionList;
     31 import com.android.dx.dex.file.ClassDefItem;
     32 import com.android.dx.dex.file.DexFile;
     33 import com.android.dx.dex.file.EncodedMethod;
     34 import com.android.dx.io.DexBuffer;
     35 import com.android.dx.merge.CollisionPolicy;
     36 import com.android.dx.merge.DexMerger;
     37 import com.android.dx.rop.annotation.Annotation;
     38 import com.android.dx.rop.annotation.Annotations;
     39 import com.android.dx.rop.annotation.AnnotationsList;
     40 import com.android.dx.rop.cst.CstNat;
     41 import com.android.dx.rop.cst.CstString;
     42 import com.android.dx.util.FileUtils;
     43 import java.io.ByteArrayInputStream;
     44 import java.io.ByteArrayOutputStream;
     45 import java.io.File;
     46 import java.io.FileOutputStream;
     47 import java.io.IOException;
     48 import java.io.OutputStream;
     49 import java.io.OutputStreamWriter;
     50 import java.io.PrintWriter;
     51 import java.util.ArrayList;
     52 import java.util.Arrays;
     53 import java.util.List;
     54 import java.util.Map;
     55 import java.util.TreeMap;
     56 import java.util.concurrent.ExecutorService;
     57 import java.util.concurrent.Executors;
     58 import java.util.concurrent.TimeUnit;
     59 import java.util.jar.Attributes;
     60 import java.util.jar.JarEntry;
     61 import java.util.jar.JarOutputStream;
     62 import java.util.jar.Manifest;
     63 
     64 /**
     65  * Main class for the class file translator.
     66  */
     67 public class Main {
     68     /**
     69      * {@code non-null;} the lengthy message that tries to discourage
     70      * people from defining core classes in applications
     71      */
     72     private static final String IN_RE_CORE_CLASSES =
     73         "Ill-advised or mistaken usage of a core class (java.* or javax.*)\n" +
     74         "when not building a core library.\n\n" +
     75         "This is often due to inadvertently including a core library file\n" +
     76         "in your application's project, when using an IDE (such as\n" +
     77         "Eclipse). If you are sure you're not intentionally defining a\n" +
     78         "core class, then this is the most likely explanation of what's\n" +
     79         "going on.\n\n" +
     80         "However, you might actually be trying to define a class in a core\n" +
     81         "namespace, the source of which you may have taken, for example,\n" +
     82         "from a non-Android virtual machine project. This will most\n" +
     83         "assuredly not work. At a minimum, it jeopardizes the\n" +
     84         "compatibility of your app with future versions of the platform.\n" +
     85         "It is also often of questionable legality.\n\n" +
     86         "If you really intend to build a core library -- which is only\n" +
     87         "appropriate as part of creating a full virtual machine\n" +
     88         "distribution, as opposed to compiling an application -- then use\n" +
     89         "the \"--core-library\" option to suppress this error message.\n\n" +
     90         "If you go ahead and use \"--core-library\" but are in fact\n" +
     91         "building an application, then be forewarned that your application\n" +
     92         "will still fail to build or run, at some point. Please be\n" +
     93         "prepared for angry customers who find, for example, that your\n" +
     94         "application ceases to function once they upgrade their operating\n" +
     95         "system. You will be to blame for this problem.\n\n" +
     96         "If you are legitimately using some code that happens to be in a\n" +
     97         "core package, then the easiest safe alternative you have is to\n" +
     98         "repackage that code. That is, move the classes in question into\n" +
     99         "your own package namespace. This means that they will never be in\n" +
    100         "conflict with core system classes. JarJar is a tool that may help\n" +
    101         "you in this endeavor. If you find that you cannot do this, then\n" +
    102         "that is an indication that the path you are on will ultimately\n" +
    103         "lead to pain, suffering, grief, and lamentation.\n";
    104 
    105     /**
    106      * {@code non-null;} name of the standard manifest file in {@code .jar}
    107      * files
    108      */
    109     private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
    110 
    111     /**
    112      * {@code non-null;} attribute name for the (quasi-standard?)
    113      * {@code Created-By} attribute
    114      */
    115     private static final Attributes.Name CREATED_BY =
    116         new Attributes.Name("Created-By");
    117 
    118     /**
    119      * {@code non-null;} list of {@code javax} subpackages that are considered
    120      * to be "core". <b>Note:</b>: This list must be sorted, since it
    121      * is binary-searched.
    122      */
    123     private static final String[] JAVAX_CORE = {
    124         "accessibility", "crypto", "imageio", "management", "naming", "net",
    125         "print", "rmi", "security", "sip", "sound", "sql", "swing",
    126         "transaction", "xml"
    127     };
    128 
    129     /** number of warnings during processing */
    130     private static int warnings = 0;
    131 
    132     /** number of errors during processing */
    133     private static int errors = 0;
    134 
    135     /** {@code non-null;} parsed command-line arguments */
    136     private static Arguments args;
    137 
    138     /** {@code non-null;} output file in-progress */
    139     private static DexFile outputDex;
    140 
    141     /**
    142      * {@code null-ok;} map of resources to include in the output, or
    143      * {@code null} if resources are being ignored
    144      */
    145     private static TreeMap<String, byte[]> outputResources;
    146 
    147     /** Library .dex files to merge into the output .dex. */
    148     private static final List<byte[]> libraryDexBuffers = new ArrayList<byte[]>();
    149 
    150     /** thread pool object used for multi-threaded file processing */
    151     private static ExecutorService threadPool;
    152 
    153     /** true if any files are successfully processed */
    154     private static boolean anyFilesProcessed;
    155 
    156     /** class files older than this must be defined in the target dex file. */
    157     private static long minimumFileAge = 0;
    158 
    159     /**
    160      * This class is uninstantiable.
    161      */
    162     private Main() {
    163         // This space intentionally left blank.
    164     }
    165 
    166     /**
    167      * Run and exit if something unexpected happened.
    168      * @param argArray the command line arguments
    169      */
    170     public static void main(String[] argArray) throws IOException {
    171         Arguments arguments = new Arguments();
    172         arguments.parse(argArray);
    173 
    174         int result = run(arguments);
    175         if (result != 0) {
    176             System.exit(result);
    177         }
    178     }
    179 
    180     /**
    181      * Run and return a result code.
    182      * @param arguments the data + parameters for the conversion
    183      * @return 0 if success > 0 otherwise.
    184      */
    185     public static int run(Arguments arguments) throws IOException {
    186         // Reset the error/warning count to start fresh.
    187         warnings = 0;
    188         errors = 0;
    189         // empty the list, so that  tools that load dx and keep it around
    190         // for multiple runs don't reuse older buffers.
    191         libraryDexBuffers.clear();
    192 
    193         args = arguments;
    194         args.makeOptionsObjects();
    195 
    196         File incrementalOutFile = null;
    197         if (args.incremental) {
    198             if (args.outName == null) {
    199                 System.err.println(
    200                         "error: no incremental output name specified");
    201                 return -1;
    202             }
    203             incrementalOutFile = new File(args.outName);
    204             if (incrementalOutFile.exists()) {
    205                 minimumFileAge = incrementalOutFile.lastModified();
    206             }
    207         }
    208 
    209         if (!processAllFiles()) {
    210             return 1;
    211         }
    212 
    213         if (args.incremental && !anyFilesProcessed) {
    214             return 0; // this was a no-op incremental build
    215         }
    216 
    217         // this array is null if no classes were defined
    218         byte[] outArray = null;
    219 
    220         if (!outputDex.isEmpty()) {
    221             outArray = writeDex();
    222 
    223             if (outArray == null) {
    224                 return 2;
    225             }
    226         }
    227 
    228         if (args.incremental) {
    229             outArray = mergeIncremental(outArray, incrementalOutFile);
    230         }
    231 
    232         outArray = mergeLibraryDexBuffers(outArray);
    233 
    234         if (args.jarOutput) {
    235             // Effectively free up the (often massive) DexFile memory.
    236             outputDex = null;
    237 
    238             if (!createJar(args.outName, outArray)) {
    239                 return 3;
    240             }
    241         } else if (outArray != null && args.outName != null) {
    242             OutputStream out = openOutput(args.outName);
    243             out.write(outArray);
    244             closeOutput(out);
    245         }
    246 
    247         return 0;
    248     }
    249 
    250     /**
    251      * Merges the dex files {@code update} and {@code base}, preferring
    252      * {@code update}'s definition for types defined in both dex files.
    253      *
    254      * @param base a file to find the previous dex file. May be a .dex file, a
    255      *     jar file possibly containing a .dex file, or null.
    256      * @return the bytes of the merged dex file, or null if both the update
    257      *     and the base dex do not exist.
    258      */
    259     private static byte[] mergeIncremental(byte[] update, File base) throws IOException {
    260         DexBuffer dexA = null;
    261         DexBuffer dexB = null;
    262 
    263         if (update != null) {
    264             dexA = new DexBuffer(update);
    265         }
    266 
    267         if (base.exists()) {
    268             dexB = new DexBuffer(base);
    269         }
    270 
    271         DexBuffer result;
    272         if (dexA == null && dexB == null) {
    273             return null;
    274         } else if (dexA == null) {
    275             result = dexB;
    276         } else if (dexB == null) {
    277             result = dexA;
    278         } else {
    279             result = new DexMerger(dexA, dexB, CollisionPolicy.KEEP_FIRST).merge();
    280         }
    281 
    282         ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
    283         result.writeTo(bytesOut);
    284         return bytesOut.toByteArray();
    285     }
    286 
    287     /**
    288      * Merges the dex files in library jars. If multiple dex files define the
    289      * same type, this fails with an exception.
    290      */
    291     private static byte[] mergeLibraryDexBuffers(byte[] outArray) throws IOException {
    292         for (byte[] libraryDexBuffer : libraryDexBuffers) {
    293             if (outArray == null) {
    294                 outArray = libraryDexBuffer;
    295                 continue;
    296             }
    297 
    298             DexBuffer a = new DexBuffer(outArray);
    299             DexBuffer b = new DexBuffer(libraryDexBuffer);
    300             DexBuffer ab = new DexMerger(a, b, CollisionPolicy.FAIL).merge();
    301             outArray = ab.getBytes();
    302         }
    303 
    304         return outArray;
    305     }
    306 
    307     /**
    308      * Constructs the output {@link DexFile}, fill it in with all the
    309      * specified classes, and populate the resources map if required.
    310      *
    311      * @return whether processing was successful
    312      */
    313     private static boolean processAllFiles() {
    314         outputDex = new DexFile(args.dexOptions);
    315 
    316         if (args.jarOutput) {
    317             outputResources = new TreeMap<String, byte[]>();
    318         }
    319 
    320         if (args.dumpWidth != 0) {
    321             outputDex.setDumpWidth(args.dumpWidth);
    322         }
    323 
    324         anyFilesProcessed = false;
    325         String[] fileNames = args.fileNames;
    326 
    327         if (args.numThreads > 1) {
    328             threadPool = Executors.newFixedThreadPool(args.numThreads);
    329         }
    330 
    331         try {
    332             for (int i = 0; i < fileNames.length; i++) {
    333                 if (processOne(fileNames[i])) {
    334                     anyFilesProcessed = true;
    335                 }
    336             }
    337         } catch (StopProcessing ex) {
    338             /*
    339              * Ignore it and just let the warning/error reporting do
    340              * their things.
    341              */
    342         }
    343 
    344         if (args.numThreads > 1) {
    345             try {
    346                 threadPool.shutdown();
    347                 threadPool.awaitTermination(600L, TimeUnit.SECONDS);
    348             } catch (InterruptedException ex) {
    349                 throw new RuntimeException("Timed out waiting for threads.");
    350             }
    351         }
    352 
    353         if (warnings != 0) {
    354             DxConsole.err.println(warnings + " warning" +
    355                                ((warnings == 1) ? "" : "s"));
    356         }
    357 
    358         if (errors != 0) {
    359             DxConsole.err.println(errors + " error" +
    360                     ((errors == 1) ? "" : "s") + "; aborting");
    361             return false;
    362         }
    363 
    364         if (args.incremental && !anyFilesProcessed) {
    365             return true;
    366         }
    367 
    368         if (!(anyFilesProcessed || args.emptyOk)) {
    369             DxConsole.err.println("no classfiles specified");
    370             return false;
    371         }
    372 
    373         if (args.optimize && args.statistics) {
    374             CodeStatistics.dumpStatistics(DxConsole.out);
    375         }
    376 
    377         return true;
    378     }
    379 
    380     /**
    381      * Processes one pathname element.
    382      *
    383      * @param pathname {@code non-null;} the pathname to process. May
    384      * be the path of a class file, a jar file, or a directory
    385      * containing class files.
    386      * @return whether any processing actually happened
    387      */
    388     private static boolean processOne(String pathname) {
    389         ClassPathOpener opener;
    390 
    391         opener = new ClassPathOpener(pathname, false,
    392                 new ClassPathOpener.Consumer() {
    393             public boolean processFileBytes(String name, long lastModified, byte[] bytes) {
    394                 if (args.numThreads > 1) {
    395                     threadPool.execute(new ParallelProcessor(name, lastModified, bytes));
    396                     return false;
    397                 } else {
    398                     return Main.processFileBytes(name, lastModified, bytes);
    399                 }
    400             }
    401             public void onException(Exception ex) {
    402                 if (ex instanceof StopProcessing) {
    403                     throw (StopProcessing) ex;
    404                 } else if (ex instanceof SimException) {
    405                     DxConsole.err.println("\nEXCEPTION FROM SIMULATION:");
    406                     DxConsole.err.println(ex.getMessage() + "\n");
    407                     DxConsole.err.println(((SimException) ex).getContext());
    408                 } else {
    409                     DxConsole.err.println("\nUNEXPECTED TOP-LEVEL EXCEPTION:");
    410                     ex.printStackTrace(DxConsole.err);
    411                 }
    412                 errors++;
    413             }
    414             public void onProcessArchiveStart(File file) {
    415                 if (args.verbose) {
    416                     DxConsole.out.println("processing archive " + file +
    417                             "...");
    418                 }
    419             }
    420         });
    421 
    422         return opener.process();
    423     }
    424 
    425     /**
    426      * Processes one file, which may be either a class or a resource.
    427      *
    428      * @param name {@code non-null;} name of the file
    429      * @param bytes {@code non-null;} contents of the file
    430      * @return whether processing was successful
    431      */
    432     private static boolean processFileBytes(String name, long lastModified, byte[] bytes) {
    433         boolean isClass = name.endsWith(".class");
    434         boolean isClassesDex = name.equals(DexFormat.DEX_IN_JAR_NAME);
    435         boolean keepResources = (outputResources != null);
    436 
    437         if (!isClass && !isClassesDex && !keepResources) {
    438             if (args.verbose) {
    439                 DxConsole.out.println("ignored resource " + name);
    440             }
    441             return false;
    442         }
    443 
    444         if (args.verbose) {
    445             DxConsole.out.println("processing " + name + "...");
    446         }
    447 
    448         String fixedName = fixPath(name);
    449 
    450         if (isClass) {
    451             if (keepResources && args.keepClassesInJar) {
    452                 synchronized (outputResources) {
    453                     outputResources.put(fixedName, bytes);
    454                 }
    455             }
    456             if (lastModified < minimumFileAge) {
    457                 return true;
    458             }
    459             return processClass(fixedName, bytes);
    460         } else if (isClassesDex) {
    461             synchronized (libraryDexBuffers) {
    462                 libraryDexBuffers.add(bytes);
    463             }
    464             return true;
    465         } else {
    466             synchronized (outputResources) {
    467                 outputResources.put(fixedName, bytes);
    468             }
    469             return true;
    470         }
    471     }
    472 
    473     /**
    474      * Processes one classfile.
    475      *
    476      * @param name {@code non-null;} name of the file, clipped such that it
    477      * <i>should</i> correspond to the name of the class it contains
    478      * @param bytes {@code non-null;} contents of the file
    479      * @return whether processing was successful
    480      */
    481     private static boolean processClass(String name, byte[] bytes) {
    482         if (! args.coreLibrary) {
    483             checkClassName(name);
    484         }
    485 
    486         try {
    487             ClassDefItem clazz =
    488                 CfTranslator.translate(name, bytes, args.cfOptions, args.dexOptions);
    489             synchronized (outputDex) {
    490                 outputDex.add(clazz);
    491             }
    492             return true;
    493         } catch (ParseException ex) {
    494             DxConsole.err.println("\ntrouble processing:");
    495             if (args.debug) {
    496                 ex.printStackTrace(DxConsole.err);
    497             } else {
    498                 ex.printContext(DxConsole.err);
    499             }
    500         }
    501 
    502         warnings++;
    503         return false;
    504     }
    505 
    506     /**
    507      * Check the class name to make sure it's not a "core library"
    508      * class. If there is a problem, this updates the error count and
    509      * throws an exception to stop processing.
    510      *
    511      * @param name {@code non-null;} the fully-qualified internal-form
    512      * class name
    513      */
    514     private static void checkClassName(String name) {
    515         boolean bogus = false;
    516 
    517         if (name.startsWith("java/")) {
    518             bogus = true;
    519         } else if (name.startsWith("javax/")) {
    520             int slashAt = name.indexOf('/', 6);
    521             if (slashAt == -1) {
    522                 // Top-level javax classes are verboten.
    523                 bogus = true;
    524             } else {
    525                 String pkg = name.substring(6, slashAt);
    526                 bogus = (Arrays.binarySearch(JAVAX_CORE, pkg) >= 0);
    527             }
    528         }
    529 
    530         if (! bogus) {
    531             return;
    532         }
    533 
    534         /*
    535          * The user is probably trying to include an entire desktop
    536          * core library in a misguided attempt to get their application
    537          * working. Try to help them understand what's happening.
    538          */
    539 
    540         DxConsole.err.println("\ntrouble processing \"" + name + "\":\n\n" +
    541                 IN_RE_CORE_CLASSES);
    542         errors++;
    543         throw new StopProcessing();
    544     }
    545 
    546     /**
    547      * Converts {@link #outputDex} into a {@code byte[]} and do whatever
    548      * human-oriented dumping is required.
    549      *
    550      * @return {@code null-ok;} the converted {@code byte[]} or {@code null}
    551      * if there was a problem
    552      */
    553     private static byte[] writeDex() {
    554         byte[] outArray = null;
    555 
    556         try {
    557             OutputStream humanOutRaw = null;
    558             OutputStreamWriter humanOut = null;
    559             try {
    560                 if (args.humanOutName != null) {
    561                     humanOutRaw = openOutput(args.humanOutName);
    562                     humanOut = new OutputStreamWriter(humanOutRaw);
    563                 }
    564 
    565                 if (args.methodToDump != null) {
    566                     /*
    567                      * Simply dump the requested method. Note: The call
    568                      * to toDex() is required just to get the underlying
    569                      * structures ready.
    570                      */
    571                     outputDex.toDex(null, false);
    572                     dumpMethod(outputDex, args.methodToDump, humanOut);
    573                 } else {
    574                     /*
    575                      * This is the usual case: Create an output .dex file,
    576                      * and write it, dump it, etc.
    577                      */
    578                     outArray = outputDex.toDex(humanOut, args.verboseDump);
    579                 }
    580 
    581                 if (args.statistics) {
    582                     DxConsole.out.println(outputDex.getStatistics().toHuman());
    583                 }
    584             } finally {
    585                 if (humanOut != null) {
    586                     humanOut.flush();
    587                 }
    588                 closeOutput(humanOutRaw);
    589             }
    590         } catch (Exception ex) {
    591             if (args.debug) {
    592                 DxConsole.err.println("\ntrouble writing output:");
    593                 ex.printStackTrace(DxConsole.err);
    594             } else {
    595                 DxConsole.err.println("\ntrouble writing output: " +
    596                                    ex.getMessage());
    597             }
    598             return null;
    599         }
    600 
    601         return outArray;
    602     }
    603 
    604     /**
    605      * Creates a jar file from the resources and given dex file array.
    606      *
    607      * @param fileName {@code non-null;} name of the file
    608      * @param dexArray array containing the dex file to include, or null if the
    609      *     output contains no class defs.
    610      * @return whether the creation was successful
    611      */
    612     private static boolean createJar(String fileName, byte[] dexArray) {
    613         /*
    614          * Make or modify the manifest (as appropriate), put the dex
    615          * array into the resources map, and then process the entire
    616          * resources map in a uniform manner.
    617          */
    618 
    619         try {
    620             Manifest manifest = makeManifest();
    621             OutputStream out = openOutput(fileName);
    622             JarOutputStream jarOut = new JarOutputStream(out, manifest);
    623 
    624             if (dexArray != null) {
    625                 outputResources.put(DexFormat.DEX_IN_JAR_NAME, dexArray);
    626             }
    627 
    628             try {
    629                 for (Map.Entry<String, byte[]> e :
    630                          outputResources.entrySet()) {
    631                     String name = e.getKey();
    632                     byte[] contents = e.getValue();
    633                     JarEntry entry = new JarEntry(name);
    634 
    635                     if (args.verbose) {
    636                         DxConsole.out.println("writing " + name + "; size " +
    637                                            contents.length + "...");
    638                     }
    639 
    640                     entry.setSize(contents.length);
    641                     jarOut.putNextEntry(entry);
    642                     jarOut.write(contents);
    643                     jarOut.closeEntry();
    644                 }
    645             } finally {
    646                 jarOut.finish();
    647                 jarOut.flush();
    648                 closeOutput(out);
    649             }
    650         } catch (Exception ex) {
    651             if (args.debug) {
    652                 DxConsole.err.println("\ntrouble writing output:");
    653                 ex.printStackTrace(DxConsole.err);
    654             } else {
    655                 DxConsole.err.println("\ntrouble writing output: " +
    656                                    ex.getMessage());
    657             }
    658             return false;
    659         }
    660 
    661         return true;
    662     }
    663 
    664     /**
    665      * Creates and returns the manifest to use for the output. This may
    666      * modify {@link #outputResources} (removing the pre-existing manifest).
    667      *
    668      * @return {@code non-null;} the manifest
    669      */
    670     private static Manifest makeManifest() throws IOException {
    671         byte[] manifestBytes = outputResources.get(MANIFEST_NAME);
    672         Manifest manifest;
    673         Attributes attribs;
    674 
    675         if (manifestBytes == null) {
    676             // We need to construct an entirely new manifest.
    677             manifest = new Manifest();
    678             attribs = manifest.getMainAttributes();
    679             attribs.put(Attributes.Name.MANIFEST_VERSION, "1.0");
    680         } else {
    681             manifest = new Manifest(new ByteArrayInputStream(manifestBytes));
    682             attribs = manifest.getMainAttributes();
    683             outputResources.remove(MANIFEST_NAME);
    684         }
    685 
    686         String createdBy = attribs.getValue(CREATED_BY);
    687         if (createdBy == null) {
    688             createdBy = "";
    689         } else {
    690             createdBy += " + ";
    691         }
    692         createdBy += "dx " + Version.VERSION;
    693 
    694         attribs.put(CREATED_BY, createdBy);
    695         attribs.putValue("Dex-Location", DexFormat.DEX_IN_JAR_NAME);
    696 
    697         return manifest;
    698     }
    699 
    700     /**
    701      * Opens and returns the named file for writing, treating "-" specially.
    702      *
    703      * @param name {@code non-null;} the file name
    704      * @return {@code non-null;} the opened file
    705      */
    706     private static OutputStream openOutput(String name) throws IOException {
    707         if (name.equals("-") ||
    708                 name.startsWith("-.")) {
    709             return System.out;
    710         }
    711 
    712         return new FileOutputStream(name);
    713     }
    714 
    715     /**
    716      * Flushes and closes the given output stream, except if it happens to be
    717      * {@link System#out} in which case this method does the flush but not
    718      * the close. This method will also silently do nothing if given a
    719      * {@code null} argument.
    720      *
    721      * @param stream {@code null-ok;} what to close
    722      */
    723     private static void closeOutput(OutputStream stream) throws IOException {
    724         if (stream == null) {
    725             return;
    726         }
    727 
    728         stream.flush();
    729 
    730         if (stream != System.out) {
    731             stream.close();
    732         }
    733     }
    734 
    735     /**
    736      * Returns the "fixed" version of a given file path, suitable for
    737      * use as a path within a {@code .jar} file and for checking
    738      * against a classfile-internal "this class" name. This looks for
    739      * the last instance of the substring {@code "/./"} within
    740      * the path, and if it finds it, it takes the portion after to be
    741      * the fixed path. If that isn't found but the path starts with
    742      * {@code "./"}, then that prefix is removed and the rest is
    743      * return. If neither of these is the case, this method returns
    744      * its argument.
    745      *
    746      * @param path {@code non-null;} the path to "fix"
    747      * @return {@code non-null;} the fixed version (which might be the same as
    748      * the given {@code path})
    749      */
    750     private static String fixPath(String path) {
    751         /*
    752          * If the path separator is \ (like on windows), we convert the
    753          * path to a standard '/' separated path.
    754          */
    755         if (File.separatorChar == '\\') {
    756             path = path.replace('\\', '/');
    757         }
    758 
    759         int index = path.lastIndexOf("/./");
    760 
    761         if (index != -1) {
    762             return path.substring(index + 3);
    763         }
    764 
    765         if (path.startsWith("./")) {
    766             return path.substring(2);
    767         }
    768 
    769         return path;
    770     }
    771 
    772     /**
    773      * Dumps any method with the given name in the given file.
    774      *
    775      * @param dex {@code non-null;} the dex file
    776      * @param fqName {@code non-null;} the fully-qualified name of the
    777      * method(s)
    778      * @param out {@code non-null;} where to dump to
    779      */
    780     private static void dumpMethod(DexFile dex, String fqName,
    781             OutputStreamWriter out) {
    782         boolean wildcard = fqName.endsWith("*");
    783         int lastDot = fqName.lastIndexOf('.');
    784 
    785         if ((lastDot <= 0) || (lastDot == (fqName.length() - 1))) {
    786             DxConsole.err.println("bogus fully-qualified method name: " +
    787                                fqName);
    788             return;
    789         }
    790 
    791         String className = fqName.substring(0, lastDot).replace('.', '/');
    792         String methodName = fqName.substring(lastDot + 1);
    793         ClassDefItem clazz = dex.getClassOrNull(className);
    794 
    795         if (clazz == null) {
    796             DxConsole.err.println("no such class: " + className);
    797             return;
    798         }
    799 
    800         if (wildcard) {
    801             methodName = methodName.substring(0, methodName.length() - 1);
    802         }
    803 
    804         ArrayList<EncodedMethod> allMeths = clazz.getMethods();
    805         TreeMap<CstNat, EncodedMethod> meths =
    806             new TreeMap<CstNat, EncodedMethod>();
    807 
    808         /*
    809          * Figure out which methods to include in the output, and get them
    810          * all sorted, so that the printout code is robust with respect to
    811          * changes in the underlying order.
    812          */
    813         for (EncodedMethod meth : allMeths) {
    814             String methName = meth.getName().getString();
    815             if ((wildcard && methName.startsWith(methodName)) ||
    816                 (!wildcard && methName.equals(methodName))) {
    817                 meths.put(meth.getRef().getNat(), meth);
    818             }
    819         }
    820 
    821         if (meths.size() == 0) {
    822             DxConsole.err.println("no such method: " + fqName);
    823             return;
    824         }
    825 
    826         PrintWriter pw = new PrintWriter(out);
    827 
    828         for (EncodedMethod meth : meths.values()) {
    829             // TODO: Better stuff goes here, perhaps.
    830             meth.debugPrint(pw, args.verboseDump);
    831 
    832             /*
    833              * The (default) source file is an attribute of the class, but
    834              * it's useful to see it in method dumps.
    835              */
    836             CstString sourceFile = clazz.getSourceFile();
    837             if (sourceFile != null) {
    838                 pw.println("  source file: " + sourceFile.toQuoted());
    839             }
    840 
    841             Annotations methodAnnotations =
    842                 clazz.getMethodAnnotations(meth.getRef());
    843             AnnotationsList parameterAnnotations =
    844                 clazz.getParameterAnnotations(meth.getRef());
    845 
    846             if (methodAnnotations != null) {
    847                 pw.println("  method annotations:");
    848                 for (Annotation a : methodAnnotations.getAnnotations()) {
    849                     pw.println("    " + a);
    850                 }
    851             }
    852 
    853             if (parameterAnnotations != null) {
    854                 pw.println("  parameter annotations:");
    855                 int sz = parameterAnnotations.size();
    856                 for (int i = 0; i < sz; i++) {
    857                     pw.println("    parameter " + i);
    858                     Annotations annotations = parameterAnnotations.get(i);
    859                     for (Annotation a : annotations.getAnnotations()) {
    860                         pw.println("      " + a);
    861                     }
    862                 }
    863             }
    864         }
    865 
    866         pw.flush();
    867     }
    868 
    869     /**
    870      * Exception class used to halt processing prematurely.
    871      */
    872     private static class StopProcessing extends RuntimeException {
    873         // This space intentionally left blank.
    874     }
    875 
    876     /**
    877      * Command-line argument parser and access.
    878      */
    879     public static class Arguments {
    880         /** whether to run in debug mode */
    881         public boolean debug = false;
    882 
    883         /** whether to emit high-level verbose human-oriented output */
    884         public boolean verbose = false;
    885 
    886         /** whether to emit verbose human-oriented output in the dump file */
    887         public boolean verboseDump = false;
    888 
    889         /** whether we are constructing a core library */
    890         public boolean coreLibrary = false;
    891 
    892         /** {@code null-ok;} particular method to dump */
    893         public String methodToDump = null;
    894 
    895         /** max width for columnar output */
    896         public int dumpWidth = 0;
    897 
    898         /** {@code null-ok;} output file name for binary file */
    899         public String outName = null;
    900 
    901         /** {@code null-ok;} output file name for human-oriented dump */
    902         public String humanOutName = null;
    903 
    904         /** whether strict file-name-vs-class-name checking should be done */
    905         public boolean strictNameCheck = true;
    906 
    907         /**
    908          * whether it is okay for there to be no {@code .class} files
    909          * to process
    910          */
    911         public boolean emptyOk = false;
    912 
    913         /**
    914          * whether the binary output is to be a {@code .jar} file
    915          * instead of a plain {@code .dex}
    916          */
    917         public boolean jarOutput = false;
    918 
    919         /**
    920          * when writing a {@code .jar} file, whether to still
    921          * keep the {@code .class} files
    922          */
    923         public boolean keepClassesInJar = false;
    924 
    925         /** what API level to target */
    926         public int targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
    927 
    928         /** how much source position info to preserve */
    929         public int positionInfo = PositionList.LINES;
    930 
    931         /** whether to keep local variable information */
    932         public boolean localInfo = true;
    933 
    934         /** whether to merge with the output dex file if it exists. */
    935         public boolean incremental = false;
    936 
    937         /** whether to force generation of const-string/jumbo for all indexes,
    938          *  to allow merges between dex files with many strings. */
    939         public boolean forceJumbo = false;
    940 
    941         /** {@code non-null} after {@link #parse}; file name arguments */
    942         public String[] fileNames;
    943 
    944         /** whether to do SSA/register optimization */
    945         public boolean optimize = true;
    946 
    947         /** Filename containg list of methods to optimize */
    948         public String optimizeListFile = null;
    949 
    950         /** Filename containing list of methods to NOT optimize */
    951         public String dontOptimizeListFile = null;
    952 
    953         /** Whether to print statistics to stdout at end of compile cycle */
    954         public boolean statistics;
    955 
    956         /** Options for class file transformation */
    957         public CfOptions cfOptions;
    958 
    959         /** Options for dex file output */
    960         public DexOptions dexOptions;
    961 
    962         /** number of threads to run with */
    963         public int numThreads = 1;
    964 
    965         private static class ArgumentsParser {
    966 
    967             /** The arguments to process. */
    968             private final String[] arguments;
    969             /** The index of the next argument to process. */
    970             private int index;
    971             /** The current argument being processed after a {@link #getNext()} call. */
    972             private String current;
    973             /** The last value of an argument processed by {@link #isArg(String)}. */
    974             private String lastValue;
    975 
    976             public ArgumentsParser(String[] arguments) {
    977                 this.arguments = arguments;
    978                 index = 0;
    979             }
    980 
    981             public String getCurrent() {
    982                 return current;
    983             }
    984 
    985             public String getLastValue() {
    986                 return lastValue;
    987             }
    988 
    989             /**
    990              * Moves on to the next argument.
    991              * Returns false when we ran out of arguments that start with --.
    992              */
    993             public boolean getNext() {
    994                 if (index >= arguments.length) {
    995                     return false;
    996                 }
    997                 current = arguments[index];
    998                 if (current.equals("--") || !current.startsWith("--")) {
    999                     return false;
   1000                 }
   1001                 index++;
   1002                 return true;
   1003             }
   1004 
   1005             /**
   1006              * Similar to {@link #getNext()}, this moves on the to next argument.
   1007              * It does not check however whether the argument starts with --
   1008              * and thus can be used to retrieve values.
   1009              */
   1010             private boolean getNextValue() {
   1011                 if (index >= arguments.length) {
   1012                     return false;
   1013                 }
   1014                 current = arguments[index];
   1015                 index++;
   1016                 return true;
   1017             }
   1018 
   1019             /**
   1020              * Returns all the arguments that have not been processed yet.
   1021              */
   1022             public String[] getRemaining() {
   1023                 int n = arguments.length - index;
   1024                 String[] remaining = new String[n];
   1025                 if (n > 0) {
   1026                     System.arraycopy(arguments, index, remaining, 0, n);
   1027                 }
   1028                 return remaining;
   1029             }
   1030 
   1031             /**
   1032              * Checks the current argument against the given prefix.
   1033              * If prefix is in the form '--name=', an extra value is expected.
   1034              * The argument can then be in the form '--name=value' or as a 2-argument
   1035              * form '--name value'.
   1036              */
   1037             public boolean isArg(String prefix) {
   1038                 int n = prefix.length();
   1039                 if (n > 0 && prefix.charAt(n-1) == '=') {
   1040                     // Argument accepts a value. Capture it.
   1041                     if (current.startsWith(prefix)) {
   1042                         // Argument is in the form --name=value, split the value out
   1043                         lastValue = current.substring(n);
   1044                         return true;
   1045                     } else {
   1046                         // Check whether we have "--name value" as 2 arguments
   1047                         prefix = prefix.substring(0, n-1);
   1048                         if (current.equals(prefix)) {
   1049                             if (getNextValue()) {
   1050                                 lastValue = current;
   1051                                 return true;
   1052                             } else {
   1053                                 System.err.println("Missing value after parameter " + prefix);
   1054                                 throw new UsageException();
   1055                             }
   1056                         }
   1057                         return false;
   1058                     }
   1059                 } else {
   1060                     // Argument does not accept a value.
   1061                     return current.equals(prefix);
   1062                 }
   1063             }
   1064         }
   1065 
   1066         /**
   1067          * Parses the given command-line arguments.
   1068          *
   1069          * @param args {@code non-null;} the arguments
   1070          */
   1071         public void parse(String[] args) {
   1072             ArgumentsParser parser = new ArgumentsParser(args);
   1073 
   1074             while(parser.getNext()) {
   1075                 if (parser.isArg("--debug")) {
   1076                     debug = true;
   1077                 } else if (parser.isArg("--verbose")) {
   1078                     verbose = true;
   1079                 } else if (parser.isArg("--verbose-dump")) {
   1080                     verboseDump = true;
   1081                 } else if (parser.isArg("--no-files")) {
   1082                     emptyOk = true;
   1083                 } else if (parser.isArg("--no-optimize")) {
   1084                     optimize = false;
   1085                 } else if (parser.isArg("--no-strict")) {
   1086                     strictNameCheck = false;
   1087                 } else if (parser.isArg("--core-library")) {
   1088                     coreLibrary = true;
   1089                 } else if (parser.isArg("--statistics")) {
   1090                     statistics = true;
   1091                 } else if (parser.isArg("--optimize-list=")) {
   1092                     if (dontOptimizeListFile != null) {
   1093                         System.err.println("--optimize-list and "
   1094                                 + "--no-optimize-list are incompatible.");
   1095                         throw new UsageException();
   1096                     }
   1097                     optimize = true;
   1098                     optimizeListFile = parser.getLastValue();
   1099                 } else if (parser.isArg("--no-optimize-list=")) {
   1100                     if (dontOptimizeListFile != null) {
   1101                         System.err.println("--optimize-list and "
   1102                                 + "--no-optimize-list are incompatible.");
   1103                         throw new UsageException();
   1104                     }
   1105                     optimize = true;
   1106                     dontOptimizeListFile = parser.getLastValue();
   1107                 } else if (parser.isArg("--keep-classes")) {
   1108                     keepClassesInJar = true;
   1109                 } else if (parser.isArg("--output=")) {
   1110                     outName = parser.getLastValue();
   1111                     if (FileUtils.hasArchiveSuffix(outName)) {
   1112                         jarOutput = true;
   1113                     } else if (outName.endsWith(".dex") ||
   1114                                outName.equals("-")) {
   1115                         jarOutput = false;
   1116                     } else {
   1117                         System.err.println("unknown output extension: " +
   1118                                            outName);
   1119                         throw new UsageException();
   1120                     }
   1121                 } else if (parser.isArg("--dump-to=")) {
   1122                     humanOutName = parser.getLastValue();
   1123                 } else if (parser.isArg("--dump-width=")) {
   1124                     dumpWidth = Integer.parseInt(parser.getLastValue());
   1125                 } else if (parser.isArg("--dump-method=")) {
   1126                     methodToDump = parser.getLastValue();
   1127                     jarOutput = false;
   1128                 } else if (parser.isArg("--positions=")) {
   1129                     String pstr = parser.getLastValue().intern();
   1130                     if (pstr == "none") {
   1131                         positionInfo = PositionList.NONE;
   1132                     } else if (pstr == "important") {
   1133                         positionInfo = PositionList.IMPORTANT;
   1134                     } else if (pstr == "lines") {
   1135                         positionInfo = PositionList.LINES;
   1136                     } else {
   1137                         System.err.println("unknown positions option: " +
   1138                                            pstr);
   1139                         throw new UsageException();
   1140                     }
   1141                 } else if (parser.isArg("--no-locals")) {
   1142                     localInfo = false;
   1143                 } else if (parser.isArg("--num-threads=")) {
   1144                     numThreads = Integer.parseInt(parser.getLastValue());
   1145                 } else if (parser.isArg("--incremental")) {
   1146                     incremental = true;
   1147                 } else if (parser.isArg("--force-jumbo")) {
   1148                     forceJumbo = true;
   1149                 } else {
   1150                     System.err.println("unknown option: " + parser.getCurrent());
   1151                     throw new UsageException();
   1152                 }
   1153             }
   1154 
   1155             fileNames = parser.getRemaining();
   1156             if (fileNames.length == 0) {
   1157                 if (!emptyOk) {
   1158                     System.err.println("no input files specified");
   1159                     throw new UsageException();
   1160                 }
   1161             } else if (emptyOk) {
   1162                 System.out.println("ignoring input files");
   1163             }
   1164 
   1165             if ((humanOutName == null) && (methodToDump != null)) {
   1166                 humanOutName = "-";
   1167             }
   1168 
   1169             makeOptionsObjects();
   1170         }
   1171 
   1172         /**
   1173          * Copies relevent arguments over into CfOptions and
   1174          * DexOptions instances.
   1175          */
   1176         private void makeOptionsObjects() {
   1177             cfOptions = new CfOptions();
   1178             cfOptions.positionInfo = positionInfo;
   1179             cfOptions.localInfo = localInfo;
   1180             cfOptions.strictNameCheck = strictNameCheck;
   1181             cfOptions.optimize = optimize;
   1182             cfOptions.optimizeListFile = optimizeListFile;
   1183             cfOptions.dontOptimizeListFile = dontOptimizeListFile;
   1184             cfOptions.statistics = statistics;
   1185             cfOptions.warn = DxConsole.err;
   1186 
   1187             dexOptions = new DexOptions();
   1188             dexOptions.targetApiLevel = targetApiLevel;
   1189             dexOptions.forceJumbo = forceJumbo;
   1190         }
   1191     }
   1192 
   1193     /** Runnable helper class to process files in multiple threads */
   1194     private static class ParallelProcessor implements Runnable {
   1195 
   1196         String path;
   1197         long lastModified;
   1198         byte[] bytes;
   1199 
   1200         /**
   1201          * Constructs an instance.
   1202          *
   1203          * @param path {@code non-null;} filename of element. May not be a valid
   1204          * filesystem path.
   1205          * @param bytes {@code non-null;} file data
   1206          */
   1207         private ParallelProcessor(String path, long lastModified, byte bytes[]) {
   1208             this.path = path;
   1209             this.lastModified = lastModified;
   1210             this.bytes = bytes;
   1211         }
   1212 
   1213         /**
   1214          * Task run by each thread in the thread pool. Runs processFileBytes
   1215          * with the given path and bytes.
   1216          */
   1217         public void run() {
   1218             if (Main.processFileBytes(path, lastModified, bytes)) {
   1219                 anyFilesProcessed = true;
   1220             }
   1221         }
   1222     }
   1223 }
   1224