OSDN Git Service

d127550a6d635fb99332d5fa700d1d6a7cdb2b95
[android-x86/dalvik.git] / dx / src / com / android / dx / command / dexer / Main.java
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 }