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