Main.java revision 333201833d506a3accdeac6ceb7caba8d4b95797
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
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();
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 = Integer.MAX_VALUE;
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        /**
958         * Parses the given command-line arguments.
959         *
960         * @param args {@code non-null;} the arguments
961         */
962        public void parse(String[] args) {
963            int at = 0;
964
965            for (/*at*/; at < args.length; at++) {
966                String arg = args[at];
967                if (arg.equals("--") || !arg.startsWith("--")) {
968                    break;
969                } else if (arg.equals("--debug")) {
970                    debug = true;
971                } else if (arg.equals("--verbose")) {
972                    verbose = true;
973                } else if (arg.equals("--verbose-dump")) {
974                    verboseDump = true;
975                } else if (arg.equals("--no-files")) {
976                    emptyOk = true;
977                } else if (arg.equals("--no-optimize")) {
978                    optimize = false;
979                } else if (arg.equals("--no-strict")) {
980                    strictNameCheck = false;
981                } else if (arg.equals("--core-library")) {
982                    coreLibrary = true;
983                } else if (arg.equals("--statistics")) {
984                    statistics = true;
985                } else if (arg.startsWith("--optimize-list=")) {
986                    if (dontOptimizeListFile != null) {
987                        System.err.println("--optimize-list and "
988                                + "--no-optimize-list are incompatible.");
989                        throw new UsageException();
990                    }
991                    optimize = true;
992                    optimizeListFile = arg.substring(arg.indexOf('=') + 1);
993                } else if (arg.startsWith("--no-optimize-list=")) {
994                    if (dontOptimizeListFile != null) {
995                        System.err.println("--optimize-list and "
996                                + "--no-optimize-list are incompatible.");
997                        throw new UsageException();
998                    }
999                    optimize = true;
1000                    dontOptimizeListFile = arg.substring(arg.indexOf('=') + 1);
1001                } else if (arg.equals("--keep-classes")) {
1002                    keepClassesInJar = true;
1003                } else if (arg.startsWith("--output=")) {
1004                    outName = arg.substring(arg.indexOf('=') + 1);
1005                    if (FileUtils.hasArchiveSuffix(outName)) {
1006                        jarOutput = true;
1007                    } else if (outName.endsWith(".dex") ||
1008                               outName.equals("-")) {
1009                        jarOutput = false;
1010                    } else {
1011                        System.err.println("unknown output extension: " +
1012                                           outName);
1013                        throw new UsageException();
1014                    }
1015                } else if (arg.startsWith("--dump-to=")) {
1016                    humanOutName = arg.substring(arg.indexOf('=') + 1);
1017                } else if (arg.startsWith("--dump-width=")) {
1018                    arg = arg.substring(arg.indexOf('=') + 1);
1019                    dumpWidth = Integer.parseInt(arg);
1020                } else if (arg.startsWith("--dump-method=")) {
1021                    methodToDump = arg.substring(arg.indexOf('=') + 1);
1022                    jarOutput = false;
1023                } else if (arg.startsWith("--target-api=")) {
1024                    arg = arg.substring(arg.indexOf('=') + 1);
1025                    int value;
1026                    try {
1027                        value = Integer.parseInt(arg);
1028                    } catch (NumberFormatException ex) {
1029                        value = -1;
1030                    }
1031                    if (value < 1) {
1032                        System.err.println("improper target-api option: " + arg);
1033                        throw new UsageException();
1034                    }
1035                    targetApiLevel = value;
1036                } else if (arg.startsWith("--positions=")) {
1037                    String pstr = arg.substring(arg.indexOf('=') + 1).intern();
1038                    if (pstr == "none") {
1039                        positionInfo = PositionList.NONE;
1040                    } else if (pstr == "important") {
1041                        positionInfo = PositionList.IMPORTANT;
1042                    } else if (pstr == "lines") {
1043                        positionInfo = PositionList.LINES;
1044                    } else {
1045                        System.err.println("unknown positions option: " +
1046                                           pstr);
1047                        throw new UsageException();
1048                    }
1049                } else if (arg.equals("--no-locals")) {
1050                    localInfo = false;
1051                } else if (arg.startsWith("--num-threads=")) {
1052                    arg = arg.substring(arg.indexOf('=') + 1);
1053                    numThreads = Integer.parseInt(arg);
1054                } else if (arg.equals("--incremental")) {
1055                    incremental = true;
1056                } else {
1057                    System.err.println("unknown option: " + arg);
1058                    throw new UsageException();
1059                }
1060            }
1061
1062            int fileCount = args.length - at;
1063
1064            if (fileCount == 0) {
1065                if (!emptyOk) {
1066                    System.err.println("no input files specified");
1067                    throw new UsageException();
1068                }
1069            } else if (emptyOk) {
1070                System.out.println("ignoring input files");
1071                at = 0;
1072                fileCount = 0;
1073            }
1074
1075            fileNames = new String[fileCount];
1076            System.arraycopy(args, at, fileNames, 0, fileCount);
1077
1078            if ((humanOutName == null) && (methodToDump != null)) {
1079                humanOutName = "-";
1080            }
1081
1082            makeOptionsObjects();
1083        }
1084
1085        /**
1086         * Copies relevent arguments over into CfOptions and
1087         * DexOptions instances.
1088         */
1089        private void makeOptionsObjects() {
1090            cfOptions = new CfOptions();
1091            cfOptions.positionInfo = positionInfo;
1092            cfOptions.localInfo = localInfo;
1093            cfOptions.strictNameCheck = strictNameCheck;
1094            cfOptions.optimize = optimize;
1095            cfOptions.optimizeListFile = optimizeListFile;
1096            cfOptions.dontOptimizeListFile = dontOptimizeListFile;
1097            cfOptions.statistics = statistics;
1098            cfOptions.warn = DxConsole.err;
1099
1100            dexOptions = new DexOptions();
1101
1102            /*
1103             * The new extended opcodes were introduced to the dex format
1104             * as of API level 13 (aka Ice Cream Sandwich).
1105             */
1106            dexOptions.enableExtendedOpcodes = targetApiLevel >= 13;
1107        }
1108    }
1109
1110    /** Runnable helper class to process files in multiple threads */
1111    private static class ParallelProcessor implements Runnable {
1112
1113        String path;
1114        long lastModified;
1115        byte[] bytes;
1116
1117        /**
1118         * Constructs an instance.
1119         *
1120         * @param path {@code non-null;} filename of element. May not be a valid
1121         * filesystem path.
1122         * @param bytes {@code non-null;} file data
1123         */
1124        private ParallelProcessor(String path, long lastModified, byte bytes[]) {
1125            this.path = path;
1126            this.lastModified = lastModified;
1127            this.bytes = bytes;
1128        }
1129
1130        /**
1131         * Task run by each thread in the thread pool. Runs processFileBytes
1132         * with the given path and bytes.
1133         */
1134        public void run() {
1135            if (Main.processFileBytes(path, lastModified, bytes)) {
1136                anyFilesProcessed = true;
1137            }
1138        }
1139    }
1140}
1141