Main.java revision 1e4c4bebc1feb4b68155e9c2e7e6f2c056ef8e3b
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dx.command.dexer;
18
19import com.android.dx.Version;
20import com.android.dx.cf.iface.ParseException;
21import com.android.dx.cf.direct.ClassPathOpener;
22import com.android.dx.command.DxConsole;
23import com.android.dx.command.UsageException;
24import com.android.dx.dex.cf.CfOptions;
25import com.android.dx.dex.cf.CfTranslator;
26import com.android.dx.dex.cf.CodeStatistics;
27import com.android.dx.dex.code.PositionList;
28import com.android.dx.dex.file.ClassDefItem;
29import com.android.dx.dex.file.DexFile;
30import com.android.dx.dex.file.EncodedMethod;
31import com.android.dx.rop.annotation.Annotation;
32import com.android.dx.rop.annotation.Annotations;
33import com.android.dx.rop.annotation.AnnotationsList;
34import com.android.dx.rop.cst.CstNat;
35import com.android.dx.rop.cst.CstUtf8;
36
37import java.io.ByteArrayInputStream;
38import java.io.File;
39import java.io.FileOutputStream;
40import java.io.IOException;
41import java.io.OutputStream;
42import java.io.OutputStreamWriter;
43import java.io.PrintWriter;
44import java.util.Arrays;
45import java.util.ArrayList;
46import java.util.Map;
47import java.util.TreeMap;
48import java.util.jar.Attributes;
49import java.util.jar.JarEntry;
50import java.util.jar.JarOutputStream;
51import java.util.jar.Manifest;
52
53/**
54 * Main class for the class file translator.
55 */
56public class Main {
57    /**
58     * {@code non-null;} the lengthy message that tries to discourage
59     * people from defining core classes in applications
60     */
61    private static final String IN_RE_CORE_CLASSES =
62        "Ill-advised or mistaken usage of a core class (java.* or javax.*)\n" +
63        "when not building a core library.\n\n" +
64        "This is often due to inadvertently including a core library file\n" +
65        "in your application's project, when using an IDE (such as\n" +
66        "Eclipse). If you are sure you're not intentionally defining a\n" +
67        "core class, then this is the most likely explanation of what's\n" +
68        "going on.\n\n" +
69        "However, you might actually be trying to define a class in a core\n" +
70        "namespace, the source of which you may have taken, for example,\n" +
71        "from a non-Android virtual machine project. This will most\n" +
72        "assuredly not work. At a minimum, it jeopardizes the\n" +
73        "compatibility of your app with future versions of the platform.\n" +
74        "It is also often of questionable legality.\n\n" +
75        "If you really intend to build a core library -- which is only\n" +
76        "appropriate as part of creating a full virtual machine\n" +
77        "distribution, as opposed to compiling an application -- then use\n" +
78        "the \"--core-library\" option to suppress this error message.\n\n" +
79        "If you go ahead and use \"--core-library\" but are in fact\n" +
80        "building an application, then be forewarned that your application\n" +
81        "will still fail to build or run, at some point. Please be\n" +
82        "prepared for angry customers who find, for example, that your\n" +
83        "application ceases to function once they upgrade their operating\n" +
84        "system. You will be to blame for this problem.\n\n" +
85        "If you are legitimately using some code that happens to be in a\n" +
86        "core package, then the easiest safe alternative you have is to\n" +
87        "repackage that code. That is, move the classes in question into\n" +
88        "your own package namespace. This means that they will never be in\n" +
89        "conflict with core system classes. JarJar is a tool that may help\n" +
90        "you in this endeavor. If you find that you cannot do this, then\n" +
91        "that is an indication that the path you are on will ultimately\n" +
92        "lead to pain, suffering, grief, and lamentation.\n";
93
94    /**
95     * {@code non-null;} name for the {@code .dex} file that goes into
96     * {@code .jar} files
97     */
98    private static final String DEX_IN_JAR_NAME = "classes.dex";
99
100    /**
101     * {@code non-null;} name of the standard manifest file in {@code .jar}
102     * files
103     */
104    private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
105
106    /**
107     * {@code non-null;} attribute name for the (quasi-standard?)
108     * {@code Created-By} attribute
109     */
110    private static final Attributes.Name CREATED_BY =
111        new Attributes.Name("Created-By");
112
113    /**
114     * {@code non-null;} list of {@code javax} subpackages that are considered
115     * to be "core". <b>Note:</b>: This list must be sorted, since it
116     * is binary-searched.
117     */
118    private static final String[] JAVAX_CORE = {
119        "accessibility", "crypto", "imageio", "management", "naming", "net",
120        "print", "rmi", "security", "sound", "sql", "swing", "transaction",
121        "xml"
122    };
123
124    /** number of warnings during processing */
125    private static int warnings = 0;
126
127    /** number of errors during processing */
128    private static int errors = 0;
129
130    /** {@code non-null;} parsed command-line arguments */
131    private static Arguments args;
132
133    /** {@code non-null;} output file in-progress */
134    private static DexFile outputDex;
135
136    /**
137     * {@code null-ok;} map of resources to include in the output, or
138     * {@code null} if resources are being ignored
139     */
140    private static TreeMap<String, byte[]> outputResources;
141
142    /**
143     * This class is uninstantiable.
144     */
145    private Main() {
146        // This space intentionally left blank.
147    }
148
149    /**
150     * Run and exit if something unexpected happened.
151     * @param argArray the command line arguments
152     */
153    public static void main(String[] argArray) {
154        Arguments arguments = new Arguments();
155        arguments.parse(argArray);
156
157        int result = run(arguments);
158        if (result != 0) {
159            System.exit(result);
160        }
161    }
162
163    /**
164     * Run and return a result code.
165     * @param arguments the data + parameters for the conversion
166     * @return 0 if success > 0 otherwise.
167     */
168    public static int run(Arguments arguments) {
169        // Reset the error/warning count to start fresh.
170        warnings = 0;
171        errors = 0;
172
173        args = arguments;
174        args.makeCfOptions();
175
176        if (!processAllFiles()) {
177            return 1;
178        }
179
180        byte[] outArray = writeDex();
181
182        if (outArray == null) {
183            return 2;
184        }
185
186        if (args.jarOutput) {
187            // Effectively free up the (often massive) DexFile memory.
188            outputDex = null;
189
190            if (!createJar(args.outName, outArray)) {
191                return 3;
192            }
193        }
194
195        return 0;
196    }
197
198    /**
199     * Constructs the output {@link DexFile}, fill it in with all the
200     * specified classes, and populate the resources map if required.
201     *
202     * @return whether processing was successful
203     */
204    private static boolean processAllFiles() {
205        outputDex = new DexFile();
206
207        if (args.jarOutput) {
208            outputResources = new TreeMap<String, byte[]>();
209        }
210
211        if (args.dumpWidth != 0) {
212            outputDex.setDumpWidth(args.dumpWidth);
213        }
214
215        boolean any = false;
216        String[] fileNames = args.fileNames;
217
218        try {
219            for (int i = 0; i < fileNames.length; i++) {
220                any |= processOne(fileNames[i]);
221            }
222        } catch (StopProcessing ex) {
223            /*
224             * Ignore it and just let the warning/error reporting do
225             * their things.
226             */
227        }
228
229        if (warnings != 0) {
230            DxConsole.err.println(warnings + " warning" +
231                               ((warnings == 1) ? "" : "s"));
232        }
233
234        if (errors != 0) {
235            DxConsole.err.println(errors + " error" +
236                    ((errors == 1) ? "" : "s") + "; aborting");
237            return false;
238        }
239
240        if (!(any || args.emptyOk)) {
241            DxConsole.err.println("no classfiles specified");
242            return false;
243        }
244
245        if (args.optimize && args.statistics) {
246            CodeStatistics.dumpStatistics(DxConsole.out);
247        }
248
249        return true;
250    }
251
252    /**
253     * Processes one pathname element.
254     *
255     * @param pathname {@code non-null;} the pathname to process. May
256     * be the path of a class file, a jar file, or a directory
257     * containing class files.
258     * @return whether any processing actually happened
259     */
260    private static boolean processOne(String pathname) {
261        ClassPathOpener opener;
262
263        opener = new ClassPathOpener(pathname, false,
264                new ClassPathOpener.Consumer() {
265            public boolean processFileBytes(String name, byte[] bytes) {
266                return Main.processFileBytes(name, bytes);
267            }
268            public void onException(Exception ex) {
269                if (ex instanceof StopProcessing) {
270                    throw (StopProcessing) ex;
271                }
272                DxConsole.err.println("\nUNEXPECTED TOP-LEVEL EXCEPTION:");
273                ex.printStackTrace(DxConsole.err);
274                errors++;
275            }
276            public void onProcessArchiveStart(File file) {
277                if (args.verbose) {
278                    DxConsole.out.println("processing archive " + file +
279                            "...");
280                }
281            }
282        });
283
284        return opener.process();
285    }
286
287    /**
288     * Processes one file, which may be either a class or a resource.
289     *
290     * @param name {@code non-null;} name of the file
291     * @param bytes {@code non-null;} contents of the file
292     * @return whether processing was successful
293     */
294    private static boolean processFileBytes(String name, byte[] bytes) {
295        boolean isClass = name.endsWith(".class");
296        boolean keepResources = (outputResources != null);
297
298        if (!isClass && !keepResources) {
299            if (args.verbose) {
300                DxConsole.out.println("ignored resource " + name);
301            }
302            return false;
303        }
304
305        if (args.verbose) {
306            DxConsole.out.println("processing " + name + "...");
307        }
308
309        String fixedName = fixPath(name);
310
311        if (isClass) {
312            if (keepResources && args.keepClassesInJar) {
313                outputResources.put(fixedName, bytes);
314            }
315            return processClass(fixedName, bytes);
316        } else {
317            outputResources.put(fixedName, bytes);
318            return true;
319        }
320    }
321
322    /**
323     * Processes one classfile.
324     *
325     * @param name {@code non-null;} name of the file, clipped such that it
326     * <i>should</i> correspond to the name of the class it contains
327     * @param bytes {@code non-null;} contents of the file
328     * @return whether processing was successful
329     */
330    private static boolean processClass(String name, byte[] bytes) {
331        if (! args.coreLibrary) {
332            checkClassName(name);
333        }
334
335        try {
336            ClassDefItem clazz =
337                CfTranslator.translate(name, bytes, args.cfOptions);
338            outputDex.add(clazz);
339            return true;
340        } catch (ParseException ex) {
341            DxConsole.err.println("\ntrouble processing:");
342            if (args.debug) {
343                ex.printStackTrace(DxConsole.err);
344            } else {
345                ex.printContext(DxConsole.err);
346            }
347        }
348
349        warnings++;
350        return false;
351    }
352
353    /**
354     * Check the class name to make sure it's not a "core library"
355     * class. If there is a problem, this updates the error count and
356     * throws an exception to stop processing.
357     *
358     * @param name {@code non-null;} the fully-qualified internal-form
359     * class name
360     */
361    private static void checkClassName(String name) {
362        boolean bogus = false;
363
364        if (name.startsWith("java/")) {
365            bogus = true;
366        } else if (name.startsWith("javax/")) {
367            int slashAt = name.indexOf('/', 6);
368            if (slashAt == -1) {
369                // Top-level javax classes are verboten.
370                bogus = true;
371            } else {
372                String pkg = name.substring(6, slashAt);
373                bogus = (Arrays.binarySearch(JAVAX_CORE, pkg) >= 0);
374            }
375        }
376
377        if (! bogus) {
378            return;
379        }
380
381        /*
382         * The user is probably trying to include an entire desktop
383         * core library in a misguided attempt to get their application
384         * working. Try to help them understand what's happening.
385         */
386
387        DxConsole.err.println("\ntrouble processing \"" + name + "\":\n\n" +
388                IN_RE_CORE_CLASSES);
389        errors++;
390        throw new StopProcessing();
391    }
392
393    /**
394     * Converts {@link #outputDex} into a {@code byte[]}, write
395     * it out to the proper file (if any), and also do whatever human-oriented
396     * dumping is required.
397     *
398     * @return {@code null-ok;} the converted {@code byte[]} or {@code null}
399     * if there was a problem
400     */
401    private static byte[] writeDex() {
402        byte[] outArray = null;
403
404        try {
405            OutputStream out = null;
406            OutputStream humanOutRaw = null;
407            OutputStreamWriter humanOut = null;
408            try {
409                if (args.humanOutName != null) {
410                    humanOutRaw = openOutput(args.humanOutName);
411                    humanOut = new OutputStreamWriter(humanOutRaw);
412                }
413
414                if (args.methodToDump != null) {
415                    /*
416                     * Simply dump the requested method. Note: The call
417                     * to toDex() is required just to get the underlying
418                     * structures ready.
419                     */
420                    outputDex.toDex(null, false);
421                    dumpMethod(outputDex, args.methodToDump, humanOut);
422                } else {
423                    /*
424                     * This is the usual case: Create an output .dex file,
425                     * and write it, dump it, etc.
426                     */
427                    outArray = outputDex.toDex(humanOut, args.verboseDump);
428
429                    if ((args.outName != null) && !args.jarOutput) {
430                        out = openOutput(args.outName);
431                        out.write(outArray);
432                    }
433                }
434
435                if (args.statistics) {
436                    DxConsole.out.println(outputDex.getStatistics().toHuman());
437                }
438            } finally {
439                if (humanOut != null) {
440                    humanOut.flush();
441                }
442                closeOutput(out);
443                closeOutput(humanOutRaw);
444            }
445        } catch (Exception ex) {
446            if (args.debug) {
447                DxConsole.err.println("\ntrouble writing output:");
448                ex.printStackTrace(DxConsole.err);
449            } else {
450                DxConsole.err.println("\ntrouble writing output: " +
451                                   ex.getMessage());
452            }
453            return null;
454        }
455
456        return outArray;
457    }
458
459    /**
460     * Creates a jar file from the resources and given dex file array.
461     *
462     * @param fileName {@code non-null;} name of the file
463     * @param dexArray {@code non-null;} array containing the dex file
464     * to include
465     * @return whether the creation was successful
466     */
467    private static boolean createJar(String fileName, byte[] dexArray) {
468        /*
469         * Make or modify the manifest (as appropriate), put the dex
470         * array into the resources map, and then process the entire
471         * resources map in a uniform manner.
472         */
473
474        try {
475            Manifest manifest = makeManifest();
476            OutputStream out = openOutput(fileName);
477            JarOutputStream jarOut = new JarOutputStream(out, manifest);
478
479            outputResources.put(DEX_IN_JAR_NAME, dexArray);
480
481            try {
482                for (Map.Entry<String, byte[]> e :
483                         outputResources.entrySet()) {
484                    String name = e.getKey();
485                    byte[] contents = e.getValue();
486                    JarEntry entry = new JarEntry(name);
487
488                    if (args.verbose) {
489                        DxConsole.out.println("writing " + name + "; size " +
490                                           contents.length + "...");
491                    }
492
493                    entry.setSize(contents.length);
494                    jarOut.putNextEntry(entry);
495                    jarOut.write(contents);
496                    jarOut.closeEntry();
497                }
498            } finally {
499                jarOut.finish();
500                jarOut.flush();
501                closeOutput(out);
502            }
503        } catch (Exception ex) {
504            if (args.debug) {
505                DxConsole.err.println("\ntrouble writing output:");
506                ex.printStackTrace(DxConsole.err);
507            } else {
508                DxConsole.err.println("\ntrouble writing output: " +
509                                   ex.getMessage());
510            }
511            return false;
512        }
513
514        return true;
515    }
516
517    /**
518     * Creates and returns the manifest to use for the output. This may
519     * modify {@link #outputResources} (removing the pre-existing manifest).
520     *
521     * @return {@code non-null;} the manifest
522     */
523    private static Manifest makeManifest() throws IOException {
524        byte[] manifestBytes = outputResources.get(MANIFEST_NAME);
525        Manifest manifest;
526        Attributes attribs;
527
528        if (manifestBytes == null) {
529            // We need to construct an entirely new manifest.
530            manifest = new Manifest();
531            attribs = manifest.getMainAttributes();
532            attribs.put(Attributes.Name.MANIFEST_VERSION, "1.0");
533        } else {
534            manifest = new Manifest(new ByteArrayInputStream(manifestBytes));
535            attribs = manifest.getMainAttributes();
536            outputResources.remove(MANIFEST_NAME);
537        }
538
539        String createdBy = attribs.getValue(CREATED_BY);
540        if (createdBy == null) {
541            createdBy = "";
542        } else {
543            createdBy += " + ";
544        }
545        createdBy += "dx " + Version.VERSION;
546
547        attribs.put(CREATED_BY, createdBy);
548        attribs.putValue("Dex-Location", DEX_IN_JAR_NAME);
549
550        return manifest;
551    }
552
553    /**
554     * Opens and returns the named file for writing, treating "-" specially.
555     *
556     * @param name {@code non-null;} the file name
557     * @return {@code non-null;} the opened file
558     */
559    private static OutputStream openOutput(String name) throws IOException {
560        if (name.equals("-") ||
561                name.startsWith("-.")) {
562            return System.out;
563        }
564
565        return new FileOutputStream(name);
566    }
567
568    /**
569     * Flushes and closes the given output stream, except if it happens to be
570     * {@link System#out} in which case this method does the flush but not
571     * the close. This method will also silently do nothing if given a
572     * {@code null} argument.
573     *
574     * @param stream {@code null-ok;} what to close
575     */
576    private static void closeOutput(OutputStream stream) throws IOException {
577        if (stream == null) {
578            return;
579        }
580
581        stream.flush();
582
583        if (stream != System.out) {
584            stream.close();
585        }
586    }
587
588    /**
589     * Returns the "fixed" version of a given file path, suitable for
590     * use as a path within a {@code .jar} file and for checking
591     * against a classfile-internal "this class" name. This looks for
592     * the last instance of the substring {@code "/./"} within
593     * the path, and if it finds it, it takes the portion after to be
594     * the fixed path. If that isn't found but the path starts with
595     * {@code "./"}, then that prefix is removed and the rest is
596     * return. If neither of these is the case, this method returns
597     * its argument.
598     *
599     * @param path {@code non-null;} the path to "fix"
600     * @return {@code non-null;} the fixed version (which might be the same as
601     * the given {@code path})
602     */
603    private static String fixPath(String path) {
604        /*
605         * If the path separator is \ (like on windows), we convert the
606         * path to a standard '/' separated path.
607         */
608        if (File.separatorChar == '\\') {
609            path = path.replace('\\', '/');
610        }
611
612        int index = path.lastIndexOf("/./");
613
614        if (index != -1) {
615            return path.substring(index + 3);
616        }
617
618        if (path.startsWith("./")) {
619            return path.substring(2);
620        }
621
622        return path;
623    }
624
625    /**
626     * Dumps any method with the given name in the given file.
627     *
628     * @param dex {@code non-null;} the dex file
629     * @param fqName {@code non-null;} the fully-qualified name of the
630     * method(s)
631     * @param out {@code non-null;} where to dump to
632     */
633    private static void dumpMethod(DexFile dex, String fqName,
634            OutputStreamWriter out) {
635        boolean wildcard = fqName.endsWith("*");
636        int lastDot = fqName.lastIndexOf('.');
637
638        if ((lastDot <= 0) || (lastDot == (fqName.length() - 1))) {
639            DxConsole.err.println("bogus fully-qualified method name: " +
640                               fqName);
641            return;
642        }
643
644        String className = fqName.substring(0, lastDot).replace('.', '/');
645        String methodName = fqName.substring(lastDot + 1);
646        ClassDefItem clazz = dex.getClassOrNull(className);
647
648        if (clazz == null) {
649            DxConsole.err.println("no such class: " + className);
650            return;
651        }
652
653        if (wildcard) {
654            methodName = methodName.substring(0, methodName.length() - 1);
655        }
656
657        ArrayList<EncodedMethod> allMeths = clazz.getMethods();
658        TreeMap<CstNat, EncodedMethod> meths =
659            new TreeMap<CstNat, EncodedMethod>();
660
661        /*
662         * Figure out which methods to include in the output, and get them
663         * all sorted, so that the printout code is robust with respect to
664         * changes in the underlying order.
665         */
666        for (EncodedMethod meth : allMeths) {
667            String methName = meth.getName().getString();
668            if ((wildcard && methName.startsWith(methodName)) ||
669                (!wildcard && methName.equals(methodName))) {
670                meths.put(meth.getRef().getNat(), meth);
671            }
672        }
673
674        if (meths.size() == 0) {
675            DxConsole.err.println("no such method: " + fqName);
676            return;
677        }
678
679        PrintWriter pw = new PrintWriter(out);
680
681        for (EncodedMethod meth : meths.values()) {
682            // TODO: Better stuff goes here, perhaps.
683            meth.debugPrint(pw, args.verboseDump);
684
685            /*
686             * The (default) source file is an attribute of the class, but
687             * it's useful to see it in method dumps.
688             */
689            CstUtf8 sourceFile = clazz.getSourceFile();
690            if (sourceFile != null) {
691                pw.println("  source file: " + sourceFile.toQuoted());
692            }
693
694            Annotations methodAnnotations =
695                clazz.getMethodAnnotations(meth.getRef());
696            AnnotationsList parameterAnnotations =
697                clazz.getParameterAnnotations(meth.getRef());
698
699            if (methodAnnotations != null) {
700                pw.println("  method annotations:");
701                for (Annotation a : methodAnnotations.getAnnotations()) {
702                    pw.println("    " + a);
703                }
704            }
705
706            if (parameterAnnotations != null) {
707                pw.println("  parameter annotations:");
708                int sz = parameterAnnotations.size();
709                for (int i = 0; i < sz; i++) {
710                    pw.println("    parameter " + i);
711                    Annotations annotations = parameterAnnotations.get(i);
712                    for (Annotation a : annotations.getAnnotations()) {
713                        pw.println("      " + a);
714                    }
715                }
716            }
717        }
718
719        pw.flush();
720    }
721
722    /**
723     * Exception class used to halt processing prematurely.
724     */
725    private static class StopProcessing extends RuntimeException {
726        // This space intentionally left blank.
727    }
728
729    /**
730     * Command-line argument parser and access.
731     */
732    public static class Arguments {
733        /** whether to run in debug mode */
734        public boolean debug = false;
735
736        /** whether to emit high-level verbose human-oriented output */
737        public boolean verbose = false;
738
739        /** whether to emit verbose human-oriented output in the dump file */
740        public boolean verboseDump = false;
741
742        /** whether we are constructing a core library */
743        public boolean coreLibrary = false;
744
745        /** {@code null-ok;} particular method to dump */
746        public String methodToDump = null;
747
748        /** max width for columnar output */
749        public int dumpWidth = 0;
750
751        /** {@code null-ok;} output file name for binary file */
752        public String outName = null;
753
754        /** {@code null-ok;} output file name for human-oriented dump */
755        public String humanOutName = null;
756
757        /** whether strict file-name-vs-class-name checking should be done */
758        public boolean strictNameCheck = true;
759
760        /**
761         * whether it is okay for there to be no {@code .class} files
762         * to process
763         */
764        public boolean emptyOk = false;
765
766        /**
767         * whether the binary output is to be a {@code .jar} file
768         * instead of a plain {@code .dex}
769         */
770        public boolean jarOutput = false;
771
772        /**
773         * when writing a {@code .jar} file, whether to still
774         * keep the {@code .class} files
775         */
776        public boolean keepClassesInJar = false;
777
778        /** how much source position info to preserve */
779        public int positionInfo = PositionList.LINES;
780
781        /** whether to keep local variable information */
782        public boolean localInfo = true;
783
784        /** {@code non-null after {@link #parse};} file name arguments */
785        public String[] fileNames;
786
787        /** whether to do SSA/register optimization */
788        public boolean optimize = true;
789
790        /** Filename containg list of methods to optimize */
791        public String optimizeListFile = null;
792
793        /** Filename containing list of methods to NOT optimize */
794        public String dontOptimizeListFile = null;
795
796        /** Whether to print statistics to stdout at end of compile cycle */
797        public boolean statistics;
798
799        /** Options for dex.cf.* */
800        public CfOptions cfOptions;
801
802        private static class ArgumentsParser {
803
804            /** The arguments to process. */
805            private final String[] arguments;
806            /** The index of the next argument to process. */
807            private int index;
808            /** The current argument being processed after a {@link #getNext()} call. */
809            private String current;
810            /** The last value of an argument processed by {@link #isArg(String)}. */
811            private String lastValue;
812
813            public ArgumentsParser(String[] arguments) {
814                this.arguments = arguments;
815                index = 0;
816            }
817
818            public String getCurrent() {
819                return current;
820            }
821
822            public String getLastValue() {
823                return lastValue;
824            }
825
826            /**
827             * Moves on to the next argument.
828             * Returns false when we ran out of arguments that start with --.
829             */
830            public boolean getNext() {
831                if (index >= arguments.length) {
832                    return false;
833                }
834                current = arguments[index];
835                if (current.equals("--") || !current.startsWith("--")) {
836                    return false;
837                }
838                index++;
839                return true;
840            }
841
842            /**
843             * Similar to {@link #getNext()}, this moves on the to next argument.
844             * It does not check however whether the argument starts with --
845             * and thus can be used to retrieve values.
846             */
847            private boolean getNextValue() {
848                if (index >= arguments.length) {
849                    return false;
850                }
851                current = arguments[index];
852                index++;
853                return true;
854            }
855
856            /**
857             * Returns all the arguments that have not been processed yet.
858             */
859            public String[] getRemaining() {
860                int n = arguments.length - index;
861                String[] remaining = new String[n];
862                if (n > 0) {
863                    System.arraycopy(arguments, index, remaining, 0, n);
864                }
865                return remaining;
866            }
867
868            /**
869             * Checks the current argument against the given prefix.
870             * If prefix is in the form '--name=', an extra value is expected.
871             * The argument can then be in the form '--name=value' or as a 2-argument
872             * form '--name value'.
873             */
874            public boolean isArg(String prefix) {
875                int n = prefix.length();
876                if (n > 0 && prefix.charAt(n-1) == '=') {
877                    // Argument accepts a value. Capture it.
878                    if (current.startsWith(prefix)) {
879                        // Argument is in the form --name=value, split the value out
880                        lastValue = current.substring(n);
881                        return true;
882                    } else {
883                        // Check whether we have "--name value" as 2 arguments
884                        prefix = prefix.substring(0, n-1);
885                        if (current.equals(prefix)) {
886                            if (getNextValue()) {
887                                lastValue = current;
888                                return true;
889                            } else {
890                                System.err.println("Missing value after parameter " + prefix);
891                                throw new UsageException();
892                            }
893                        }
894                        return false;
895                    }
896                } else {
897                    // Argument does not accept a value.
898                    return current.equals(prefix);
899                }
900            }
901        }
902
903        /**
904         * Parses the given command-line arguments.
905         *
906         * @param args {@code non-null;} the arguments
907         */
908        public void parse(String[] args) {
909            ArgumentsParser parser = new ArgumentsParser(args);
910
911            while(parser.getNext()) {
912                if (parser.isArg("--debug")) {
913                    debug = true;
914                } else if (parser.isArg("--verbose")) {
915                    verbose = true;
916                } else if (parser.isArg("--verbose-dump")) {
917                    verboseDump = true;
918                } else if (parser.isArg("--no-files")) {
919                    emptyOk = true;
920                } else if (parser.isArg("--no-optimize")) {
921                    optimize = false;
922                } else if (parser.isArg("--no-strict")) {
923                    strictNameCheck = false;
924                } else if (parser.isArg("--core-library")) {
925                    coreLibrary = true;
926                } else if (parser.isArg("--statistics")) {
927                    statistics = true;
928                } else if (parser.isArg("--optimize-list=")) {
929                    if (dontOptimizeListFile != null) {
930                        System.err.println("--optimize-list and "
931                                + "--no-optimize-list are incompatible.");
932                        throw new UsageException();
933                    }
934                    optimize = true;
935                    optimizeListFile = parser.getLastValue();
936                } else if (parser.isArg("--no-optimize-list=")) {
937                    if (dontOptimizeListFile != null) {
938                        System.err.println("--optimize-list and "
939                                + "--no-optimize-list are incompatible.");
940                        throw new UsageException();
941                    }
942                    optimize = true;
943                    dontOptimizeListFile = parser.getLastValue();
944                } else if (parser.isArg("--keep-classes")) {
945                    keepClassesInJar = true;
946                } else if (parser.isArg("--output=")) {
947                    outName = parser.getLastValue();
948                    if (outName.endsWith(".zip") ||
949                            outName.endsWith(".jar") ||
950                            outName.endsWith(".apk")) {
951                        jarOutput = true;
952                    } else if (outName.endsWith(".dex") ||
953                               outName.equals("-")) {
954                        jarOutput = false;
955                    } else {
956                        System.err.println("unknown output extension: " +
957                                           outName);
958                        throw new UsageException();
959                    }
960                } else if (parser.isArg("--dump-to=")) {
961                    humanOutName = parser.getLastValue();
962                } else if (parser.isArg("--dump-width=")) {
963                    dumpWidth = Integer.parseInt(parser.getLastValue());
964                } else if (parser.isArg("--dump-method=")) {
965                    methodToDump = parser.getLastValue();
966                    jarOutput = false;
967                } else if (parser.isArg("--positions=")) {
968                    String pstr = parser.getLastValue().intern();
969                    if (pstr == "none") {
970                        positionInfo = PositionList.NONE;
971                    } else if (pstr == "important") {
972                        positionInfo = PositionList.IMPORTANT;
973                    } else if (pstr == "lines") {
974                        positionInfo = PositionList.LINES;
975                    } else {
976                        System.err.println("unknown positions option: " +
977                                           pstr);
978                        throw new UsageException();
979                    }
980                } else if (parser.isArg("--no-locals")) {
981                    localInfo = false;
982                } else {
983                    System.err.println("unknown option: " + parser.getCurrent());
984                    throw new UsageException();
985                }
986            }
987
988            fileNames = parser.getRemaining();
989            if (fileNames.length == 0) {
990                if (!emptyOk) {
991                    System.err.println("no input files specified");
992                    throw new UsageException();
993                }
994            } else if (emptyOk) {
995                System.out.println("ignoring input files");
996            }
997
998            if ((humanOutName == null) && (methodToDump != null)) {
999                humanOutName = "-";
1000            }
1001
1002            makeCfOptions();
1003        }
1004
1005        /**
1006         * Copies relevent arguments over into a CfOptions instance.
1007         */
1008        private void makeCfOptions() {
1009            cfOptions = new CfOptions();
1010
1011            cfOptions.positionInfo = positionInfo;
1012            cfOptions.localInfo = localInfo;
1013            cfOptions.strictNameCheck = strictNameCheck;
1014            cfOptions.optimize = optimize;
1015            cfOptions.optimizeListFile = optimizeListFile;
1016            cfOptions.dontOptimizeListFile = dontOptimizeListFile;
1017            cfOptions.statistics = statistics;
1018            cfOptions.warn = DxConsole.err;
1019        }
1020    }
1021}
1022