Main.java revision 5e90165d5918f18aec0964bc475f4064e9143599
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
17package com.android.dx.command.dexer;
18
19import com.android.dx.Version;
20import com.android.dx.cf.code.SimException;
21import com.android.dx.cf.direct.ClassPathOpener;
22import com.android.dx.cf.iface.ParseException;
23import com.android.dx.command.DxConsole;
24import com.android.dx.command.UsageException;
25import com.android.dx.dex.DexFormat;
26import com.android.dx.dex.DexOptions;
27import com.android.dx.dex.cf.CfOptions;
28import com.android.dx.dex.cf.CfTranslator;
29import com.android.dx.dex.cf.CodeStatistics;
30import com.android.dx.dex.code.PositionList;
31import com.android.dx.dex.file.ClassDefItem;
32import com.android.dx.dex.file.DexFile;
33import com.android.dx.dex.file.EncodedMethod;
34import com.android.dx.io.DexBuffer;
35import com.android.dx.merge.CollisionPolicy;
36import com.android.dx.merge.DexMerger;
37import com.android.dx.rop.annotation.Annotation;
38import com.android.dx.rop.annotation.Annotations;
39import com.android.dx.rop.annotation.AnnotationsList;
40import com.android.dx.rop.cst.CstNat;
41import com.android.dx.rop.cst.CstString;
42import com.android.dx.util.FileUtils;
43import java.io.ByteArrayInputStream;
44import java.io.ByteArrayOutputStream;
45import java.io.File;
46import java.io.FileOutputStream;
47import java.io.IOException;
48import java.io.OutputStream;
49import java.io.OutputStreamWriter;
50import java.io.PrintWriter;
51import java.util.ArrayList;
52import java.util.Arrays;
53import java.util.List;
54import java.util.Map;
55import java.util.TreeMap;
56import java.util.concurrent.ExecutorService;
57import java.util.concurrent.Executors;
58import java.util.concurrent.TimeUnit;
59import java.util.jar.Attributes;
60import java.util.jar.JarEntry;
61import java.util.jar.JarOutputStream;
62import java.util.jar.Manifest;
63
64/**
65 * Main class for the class file translator.
66 */
67public 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        /** {@code non-null} after {@link #parse}; file name arguments */
938        public String[] fileNames;
939
940        /** whether to do SSA/register optimization */
941        public boolean optimize = true;
942
943        /** Filename containg list of methods to optimize */
944        public String optimizeListFile = null;
945
946        /** Filename containing list of methods to NOT optimize */
947        public String dontOptimizeListFile = null;
948
949        /** Whether to print statistics to stdout at end of compile cycle */
950        public boolean statistics;
951
952        /** Options for class file transformation */
953        public CfOptions cfOptions;
954
955        /** Options for dex file output */
956        public DexOptions dexOptions;
957
958        /** number of threads to run with */
959        public int numThreads = 1;
960
961        private static class ArgumentsParser {
962
963            /** The arguments to process. */
964            private final String[] arguments;
965            /** The index of the next argument to process. */
966            private int index;
967            /** The current argument being processed after a {@link #getNext()} call. */
968            private String current;
969            /** The last value of an argument processed by {@link #isArg(String)}. */
970            private String lastValue;
971
972            public ArgumentsParser(String[] arguments) {
973                this.arguments = arguments;
974                index = 0;
975            }
976
977            public String getCurrent() {
978                return current;
979            }
980
981            public String getLastValue() {
982                return lastValue;
983            }
984
985            /**
986             * Moves on to the next argument.
987             * Returns false when we ran out of arguments that start with --.
988             */
989            public boolean getNext() {
990                if (index >= arguments.length) {
991                    return false;
992                }
993                current = arguments[index];
994                if (current.equals("--") || !current.startsWith("--")) {
995                    return false;
996                }
997                index++;
998                return true;
999            }
1000
1001            /**
1002             * Similar to {@link #getNext()}, this moves on the to next argument.
1003             * It does not check however whether the argument starts with --
1004             * and thus can be used to retrieve values.
1005             */
1006            private boolean getNextValue() {
1007                if (index >= arguments.length) {
1008                    return false;
1009                }
1010                current = arguments[index];
1011                index++;
1012                return true;
1013            }
1014
1015            /**
1016             * Returns all the arguments that have not been processed yet.
1017             */
1018            public String[] getRemaining() {
1019                int n = arguments.length - index;
1020                String[] remaining = new String[n];
1021                if (n > 0) {
1022                    System.arraycopy(arguments, index, remaining, 0, n);
1023                }
1024                return remaining;
1025            }
1026
1027            /**
1028             * Checks the current argument against the given prefix.
1029             * If prefix is in the form '--name=', an extra value is expected.
1030             * The argument can then be in the form '--name=value' or as a 2-argument
1031             * form '--name value'.
1032             */
1033            public boolean isArg(String prefix) {
1034                int n = prefix.length();
1035                if (n > 0 && prefix.charAt(n-1) == '=') {
1036                    // Argument accepts a value. Capture it.
1037                    if (current.startsWith(prefix)) {
1038                        // Argument is in the form --name=value, split the value out
1039                        lastValue = current.substring(n);
1040                        return true;
1041                    } else {
1042                        // Check whether we have "--name value" as 2 arguments
1043                        prefix = prefix.substring(0, n-1);
1044                        if (current.equals(prefix)) {
1045                            if (getNextValue()) {
1046                                lastValue = current;
1047                                return true;
1048                            } else {
1049                                System.err.println("Missing value after parameter " + prefix);
1050                                throw new UsageException();
1051                            }
1052                        }
1053                        return false;
1054                    }
1055                } else {
1056                    // Argument does not accept a value.
1057                    return current.equals(prefix);
1058                }
1059            }
1060        }
1061
1062        /**
1063         * Parses the given command-line arguments.
1064         *
1065         * @param args {@code non-null;} the arguments
1066         */
1067        public void parse(String[] args) {
1068            ArgumentsParser parser = new ArgumentsParser(args);
1069
1070            while(parser.getNext()) {
1071                if (parser.isArg("--debug")) {
1072                    debug = true;
1073                } else if (parser.isArg("--verbose")) {
1074                    verbose = true;
1075                } else if (parser.isArg("--verbose-dump")) {
1076                    verboseDump = true;
1077                } else if (parser.isArg("--no-files")) {
1078                    emptyOk = true;
1079                } else if (parser.isArg("--no-optimize")) {
1080                    optimize = false;
1081                } else if (parser.isArg("--no-strict")) {
1082                    strictNameCheck = false;
1083                } else if (parser.isArg("--core-library")) {
1084                    coreLibrary = true;
1085                } else if (parser.isArg("--statistics")) {
1086                    statistics = true;
1087                } else if (parser.isArg("--optimize-list=")) {
1088                    if (dontOptimizeListFile != null) {
1089                        System.err.println("--optimize-list and "
1090                                + "--no-optimize-list are incompatible.");
1091                        throw new UsageException();
1092                    }
1093                    optimize = true;
1094                    optimizeListFile = parser.getLastValue();
1095                } else if (parser.isArg("--no-optimize-list=")) {
1096                    if (dontOptimizeListFile != null) {
1097                        System.err.println("--optimize-list and "
1098                                + "--no-optimize-list are incompatible.");
1099                        throw new UsageException();
1100                    }
1101                    optimize = true;
1102                    dontOptimizeListFile = parser.getLastValue();
1103                } else if (parser.isArg("--keep-classes")) {
1104                    keepClassesInJar = true;
1105                } else if (parser.isArg("--output=")) {
1106                    outName = parser.getLastValue();
1107                    if (FileUtils.hasArchiveSuffix(outName)) {
1108                        jarOutput = true;
1109                    } else if (outName.endsWith(".dex") ||
1110                               outName.equals("-")) {
1111                        jarOutput = false;
1112                    } else {
1113                        System.err.println("unknown output extension: " +
1114                                           outName);
1115                        throw new UsageException();
1116                    }
1117                } else if (parser.isArg("--dump-to=")) {
1118                    humanOutName = parser.getLastValue();
1119                } else if (parser.isArg("--dump-width=")) {
1120                    dumpWidth = Integer.parseInt(parser.getLastValue());
1121                } else if (parser.isArg("--dump-method=")) {
1122                    methodToDump = parser.getLastValue();
1123                    jarOutput = false;
1124                } else if (parser.isArg("--positions=")) {
1125                    String pstr = parser.getLastValue().intern();
1126                    if (pstr == "none") {
1127                        positionInfo = PositionList.NONE;
1128                    } else if (pstr == "important") {
1129                        positionInfo = PositionList.IMPORTANT;
1130                    } else if (pstr == "lines") {
1131                        positionInfo = PositionList.LINES;
1132                    } else {
1133                        System.err.println("unknown positions option: " +
1134                                           pstr);
1135                        throw new UsageException();
1136                    }
1137                } else if (parser.isArg("--no-locals")) {
1138                    localInfo = false;
1139                } else if (parser.isArg("--num-threads=")) {
1140                    numThreads = Integer.parseInt(parser.getLastValue());
1141                } else if (parser.isArg("--incremental")) {
1142                    incremental = true;
1143                } else {
1144                    System.err.println("unknown option: " + parser.getCurrent());
1145                    throw new UsageException();
1146                }
1147            }
1148
1149            fileNames = parser.getRemaining();
1150            if (fileNames.length == 0) {
1151                if (!emptyOk) {
1152                    System.err.println("no input files specified");
1153                    throw new UsageException();
1154                }
1155            } else if (emptyOk) {
1156                System.out.println("ignoring input files");
1157            }
1158
1159            if ((humanOutName == null) && (methodToDump != null)) {
1160                humanOutName = "-";
1161            }
1162
1163            makeOptionsObjects();
1164        }
1165
1166        /**
1167         * Copies relevent arguments over into CfOptions and
1168         * DexOptions instances.
1169         */
1170        private void makeOptionsObjects() {
1171            cfOptions = new CfOptions();
1172            cfOptions.positionInfo = positionInfo;
1173            cfOptions.localInfo = localInfo;
1174            cfOptions.strictNameCheck = strictNameCheck;
1175            cfOptions.optimize = optimize;
1176            cfOptions.optimizeListFile = optimizeListFile;
1177            cfOptions.dontOptimizeListFile = dontOptimizeListFile;
1178            cfOptions.statistics = statistics;
1179            cfOptions.warn = DxConsole.err;
1180
1181            dexOptions = new DexOptions();
1182            dexOptions.targetApiLevel = targetApiLevel;
1183        }
1184    }
1185
1186    /** Runnable helper class to process files in multiple threads */
1187    private static class ParallelProcessor implements Runnable {
1188
1189        String path;
1190        long lastModified;
1191        byte[] bytes;
1192
1193        /**
1194         * Constructs an instance.
1195         *
1196         * @param path {@code non-null;} filename of element. May not be a valid
1197         * filesystem path.
1198         * @param bytes {@code non-null;} file data
1199         */
1200        private ParallelProcessor(String path, long lastModified, byte bytes[]) {
1201            this.path = path;
1202            this.lastModified = lastModified;
1203            this.bytes = bytes;
1204        }
1205
1206        /**
1207         * Task run by each thread in the thread pool. Runs processFileBytes
1208         * with the given path and bytes.
1209         */
1210        public void run() {
1211            if (Main.processFileBytes(path, lastModified, bytes)) {
1212                anyFilesProcessed = true;
1213            }
1214        }
1215    }
1216}
1217