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