Main.java revision 4c656e4ec2f5c5036dc67fb4034c1e7ff7abf343
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.dex.Dex;
20import com.android.dex.DexException;
21import com.android.dex.DexFormat;
22import com.android.dex.util.FileUtils;
23import com.android.dx.Version;
24import com.android.dx.cf.code.SimException;
25import com.android.dx.cf.direct.ClassPathOpener;
26import com.android.dx.cf.direct.ClassPathOpener.FileNameFilter;
27import com.android.dx.cf.direct.DirectClassFile;
28import com.android.dx.cf.direct.StdAttributeFactory;
29import com.android.dx.cf.iface.ParseException;
30import com.android.dx.command.DxConsole;
31import com.android.dx.command.UsageException;
32import com.android.dx.dex.DexOptions;
33import com.android.dx.dex.cf.CfOptions;
34import com.android.dx.dex.cf.CfTranslator;
35import com.android.dx.dex.cf.CodeStatistics;
36import com.android.dx.dex.code.PositionList;
37import com.android.dx.dex.file.AnnotationUtils;
38import com.android.dx.dex.file.ClassDefItem;
39import com.android.dx.dex.file.DexFile;
40import com.android.dx.dex.file.EncodedMethod;
41import com.android.dx.merge.CollisionPolicy;
42import com.android.dx.merge.DexMerger;
43import com.android.dx.rop.annotation.Annotation;
44import com.android.dx.rop.annotation.Annotations;
45import com.android.dx.rop.annotation.AnnotationsList;
46import com.android.dx.rop.cst.CstNat;
47import com.android.dx.rop.cst.CstString;
48
49import java.io.BufferedReader;
50import java.io.ByteArrayInputStream;
51import java.io.ByteArrayOutputStream;
52import java.io.File;
53import java.io.FileOutputStream;
54import java.io.FileReader;
55import java.io.IOException;
56import java.io.OutputStream;
57import java.io.OutputStreamWriter;
58import java.io.PrintWriter;
59import java.util.ArrayList;
60import java.util.Arrays;
61import java.util.HashMap;
62import java.util.HashSet;
63import java.util.List;
64import java.util.Map;
65import java.util.Set;
66import java.util.TreeMap;
67import java.util.concurrent.ExecutorService;
68import java.util.concurrent.Executors;
69import java.util.concurrent.TimeUnit;
70import java.util.jar.Attributes;
71import java.util.jar.JarEntry;
72import java.util.jar.JarOutputStream;
73import java.util.jar.Manifest;
74
75/**
76 * Main class for the class file translator.
77 */
78public class Main {
79    /**
80     * {@code non-null;} Error message for too many method/field/type ids.
81     */
82    public static final String TO_MANY_ID_ERROR_MESSAGE =
83        "Dex limit exceeded. You may try option " + Arguments.MULTI_DEX_OPTION;
84
85    /**
86     * File extension of a {@code .dex} file.
87     */
88    private static final String DEX_EXTENSION = ".dex";
89
90    /**
91     * File name prefix of a {@code .dex} file automatically loaded in an
92     * archive.
93     */
94    private static final String DEX_PREFIX = "classes";
95
96    /**
97     * {@code non-null;} the lengthy message that tries to discourage
98     * people from defining core classes in applications
99     */
100    private static final String IN_RE_CORE_CLASSES =
101        "Ill-advised or mistaken usage of a core class (java.* or javax.*)\n" +
102        "when not building a core library.\n\n" +
103        "This is often due to inadvertently including a core library file\n" +
104        "in your application's project, when using an IDE (such as\n" +
105        "Eclipse). If you are sure you're not intentionally defining a\n" +
106        "core class, then this is the most likely explanation of what's\n" +
107        "going on.\n\n" +
108        "However, you might actually be trying to define a class in a core\n" +
109        "namespace, the source of which you may have taken, for example,\n" +
110        "from a non-Android virtual machine project. This will most\n" +
111        "assuredly not work. At a minimum, it jeopardizes the\n" +
112        "compatibility of your app with future versions of the platform.\n" +
113        "It is also often of questionable legality.\n\n" +
114        "If you really intend to build a core library -- which is only\n" +
115        "appropriate as part of creating a full virtual machine\n" +
116        "distribution, as opposed to compiling an application -- then use\n" +
117        "the \"--core-library\" option to suppress this error message.\n\n" +
118        "If you go ahead and use \"--core-library\" but are in fact\n" +
119        "building an application, then be forewarned that your application\n" +
120        "will still fail to build or run, at some point. Please be\n" +
121        "prepared for angry customers who find, for example, that your\n" +
122        "application ceases to function once they upgrade their operating\n" +
123        "system. You will be to blame for this problem.\n\n" +
124        "If you are legitimately using some code that happens to be in a\n" +
125        "core package, then the easiest safe alternative you have is to\n" +
126        "repackage that code. That is, move the classes in question into\n" +
127        "your own package namespace. This means that they will never be in\n" +
128        "conflict with core system classes. JarJar is a tool that may help\n" +
129        "you in this endeavor. If you find that you cannot do this, then\n" +
130        "that is an indication that the path you are on will ultimately\n" +
131        "lead to pain, suffering, grief, and lamentation.\n";
132
133    /**
134     * {@code non-null;} name of the standard manifest file in {@code .jar}
135     * files
136     */
137    private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
138
139    /**
140     * {@code non-null;} attribute name for the (quasi-standard?)
141     * {@code Created-By} attribute
142     */
143    private static final Attributes.Name CREATED_BY =
144        new Attributes.Name("Created-By");
145
146    /**
147     * {@code non-null;} list of {@code javax} subpackages that are considered
148     * to be "core". <b>Note:</b>: This list must be sorted, since it
149     * is binary-searched.
150     */
151    private static final String[] JAVAX_CORE = {
152        "accessibility", "crypto", "imageio", "management", "naming", "net",
153        "print", "rmi", "security", "sip", "sound", "sql", "swing",
154        "transaction", "xml"
155    };
156
157    /** number of errors during processing */
158    private static int errors = 0;
159
160    /** {@code non-null;} parsed command-line arguments */
161    private static Arguments args;
162
163    /** {@code non-null;} output file in-progress */
164    private static DexFile outputDex;
165
166    /**
167     * {@code null-ok;} map of resources to include in the output, or
168     * {@code null} if resources are being ignored
169     */
170    private static TreeMap<String, byte[]> outputResources;
171
172    /** Library .dex files to merge into the output .dex. */
173    private static final List<byte[]> libraryDexBuffers = new ArrayList<byte[]>();
174
175    /** thread pool object used for multi-threaded file processing */
176    private static ExecutorService threadPool;
177
178    /** true if any files are successfully processed */
179    private static boolean anyFilesProcessed;
180
181    /** class files older than this must be defined in the target dex file. */
182    private static long minimumFileAge = 0;
183
184    private static Set<String> classesInMainDex = null;
185
186    private static List<byte[]> dexOutputArrays = new ArrayList<byte[]>();
187
188    private static OutputStreamWriter humanOutWriter = null;
189
190    /**
191     * This class is uninstantiable.
192     */
193    private Main() {
194        // This space intentionally left blank.
195    }
196
197    /**
198     * Run and exit if something unexpected happened.
199     * @param argArray the command line arguments
200     */
201    public static void main(String[] argArray) throws IOException {
202        Arguments arguments = new Arguments();
203        arguments.parse(argArray);
204
205        int result = run(arguments);
206        if (result != 0) {
207            System.exit(result);
208        }
209    }
210
211    /**
212     * Run and return a result code.
213     * @param arguments the data + parameters for the conversion
214     * @return 0 if success > 0 otherwise.
215     */
216    public static int run(Arguments arguments) throws IOException {
217        // Reset the error count to start fresh.
218        errors = 0;
219        // empty the list, so that  tools that load dx and keep it around
220        // for multiple runs don't reuse older buffers.
221        libraryDexBuffers.clear();
222
223        args = arguments;
224        args.makeOptionsObjects();
225
226        OutputStream humanOutRaw = null;
227        if (args.humanOutName != null) {
228            humanOutRaw = openOutput(args.humanOutName);
229            humanOutWriter = new OutputStreamWriter(humanOutRaw);
230        }
231
232        try {
233            if (args.multiDex) {
234                return runMultiDex();
235            } else {
236                return runMonoDex();
237            }
238        } finally {
239            closeOutput(humanOutRaw);
240        }
241    }
242
243    private static int runMonoDex() throws IOException {
244
245        File incrementalOutFile = null;
246        if (args.incremental) {
247            if (args.outName == null) {
248                System.err.println(
249                        "error: no incremental output name specified");
250                return -1;
251            }
252            incrementalOutFile = new File(args.outName);
253            if (incrementalOutFile.exists()) {
254                minimumFileAge = incrementalOutFile.lastModified();
255            }
256        }
257
258        if (!processAllFiles()) {
259            return 1;
260        }
261
262        if (args.incremental && !anyFilesProcessed) {
263            return 0; // this was a no-op incremental build
264        }
265
266        // this array is null if no classes were defined
267        byte[] outArray = null;
268
269        if (!outputDex.isEmpty()) {
270            outArray = writeDex();
271
272            if (outArray == null) {
273                return 2;
274            }
275        }
276
277        if (args.incremental) {
278            outArray = mergeIncremental(outArray, incrementalOutFile);
279        }
280
281        outArray = mergeLibraryDexBuffers(outArray);
282
283        if (args.jarOutput) {
284            // Effectively free up the (often massive) DexFile memory.
285            outputDex = null;
286
287            if (outArray != null) {
288                outputResources.put(DexFormat.DEX_IN_JAR_NAME, outArray);
289            }
290            if (!createJar(args.outName)) {
291                return 3;
292            }
293        } else if (outArray != null && args.outName != null) {
294            OutputStream out = openOutput(args.outName);
295            out.write(outArray);
296            closeOutput(out);
297        }
298
299        return 0;
300    }
301
302    private static int runMultiDex() throws IOException {
303
304        assert !args.incremental;
305        assert args.numThreads == 1;
306
307        if (args.mainDexListFile != null) {
308            classesInMainDex = loadMainDexListFile(args.mainDexListFile);
309        }
310
311        if (!processAllFiles()) {
312            return 1;
313        }
314
315        if (!libraryDexBuffers.isEmpty()) {
316            throw new DexException("Library dex files are not supported in multi-dex mode");
317        }
318
319        if (outputDex != null) {
320            // this array is null if no classes were defined
321            dexOutputArrays.add(writeDex());
322
323            // Effectively free up the (often massive) DexFile memory.
324            outputDex = null;
325        }
326
327        if (args.jarOutput) {
328
329            for (int i = 0; i < dexOutputArrays.size(); i++) {
330                outputResources.put(getDexFileName(i),
331                        dexOutputArrays.get(i));
332            }
333
334            if (!createJar(args.outName)) {
335                return 3;
336            }
337        } else if (args.outName != null) {
338            File outDir = new File(args.outName);
339            assert outDir.isDirectory();
340            for (int i = 0; i < dexOutputArrays.size(); i++) {
341                OutputStream out = new FileOutputStream(new File(outDir, getDexFileName(i)));
342                try {
343                    out.write(dexOutputArrays.get(i));
344                } finally {
345                    closeOutput(out);
346                }
347            }
348
349        }
350
351        return 0;
352    }
353
354    private static String getDexFileName(int i) {
355        if (i == 0) {
356            return DexFormat.DEX_IN_JAR_NAME;
357        } else {
358            return DEX_PREFIX + (i + 1) + DEX_EXTENSION;
359        }
360    }
361
362    private static Set<String> loadMainDexListFile(String mainDexListFile) throws IOException {
363        Set<String> mainDexList = new HashSet<String>();
364        BufferedReader bfr = null;
365        try {
366            FileReader fr = new FileReader(mainDexListFile);
367            bfr = new BufferedReader(fr);
368
369            String line;
370
371            while (null != (line = bfr.readLine())) {
372                mainDexList.add(fixPath(line));
373            }
374
375        } finally {
376            if (bfr != null) {
377                bfr.close();
378            }
379        }
380        return mainDexList;
381    }
382
383    /**
384     * Merges the dex files {@code update} and {@code base}, preferring
385     * {@code update}'s definition for types defined in both dex files.
386     *
387     * @param base a file to find the previous dex file. May be a .dex file, a
388     *     jar file possibly containing a .dex file, or null.
389     * @return the bytes of the merged dex file, or null if both the update
390     *     and the base dex do not exist.
391     */
392    private static byte[] mergeIncremental(byte[] update, File base) throws IOException {
393        Dex dexA = null;
394        Dex dexB = null;
395
396        if (update != null) {
397            dexA = new Dex(update);
398        }
399
400        if (base.exists()) {
401            dexB = new Dex(base);
402        }
403
404        Dex result;
405        if (dexA == null && dexB == null) {
406            return null;
407        } else if (dexA == null) {
408            result = dexB;
409        } else if (dexB == null) {
410            result = dexA;
411        } else {
412            result = new DexMerger(dexA, dexB, CollisionPolicy.KEEP_FIRST).merge();
413        }
414
415        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
416        result.writeTo(bytesOut);
417        return bytesOut.toByteArray();
418    }
419
420    /**
421     * Merges the dex files in library jars. If multiple dex files define the
422     * same type, this fails with an exception.
423     */
424    private static byte[] mergeLibraryDexBuffers(byte[] outArray) throws IOException {
425        for (byte[] libraryDex : libraryDexBuffers) {
426            if (outArray == null) {
427                outArray = libraryDex;
428                continue;
429            }
430
431            Dex a = new Dex(outArray);
432            Dex b = new Dex(libraryDex);
433            Dex ab = new DexMerger(a, b, CollisionPolicy.FAIL).merge();
434            outArray = ab.getBytes();
435        }
436
437        return outArray;
438    }
439
440    /**
441     * Constructs the output {@link DexFile}, fill it in with all the
442     * specified classes, and populate the resources map if required.
443     *
444     * @return whether processing was successful
445     */
446    private static boolean processAllFiles() {
447        createDexFile();
448
449        if (args.jarOutput) {
450            outputResources = new TreeMap<String, byte[]>();
451        }
452
453        anyFilesProcessed = false;
454        String[] fileNames = args.fileNames;
455
456        if (args.numThreads > 1) {
457            threadPool = Executors.newFixedThreadPool(args.numThreads);
458        }
459
460        try {
461            if (args.mainDexListFile != null) {
462                // with --main-dex-list
463                FileNameFilter mainPassFilter = args.strictNameCheck ? new MainDexListFilter() :
464                    new BestEffortMainDexListFilter();
465
466                // forced in main dex
467                for (int i = 0; i < fileNames.length; i++) {
468                    if (processOne(fileNames[i], mainPassFilter)) {
469                        anyFilesProcessed = true;
470                    }
471                }
472
473                if (dexOutputArrays.size() > 1) {
474                    throw new DexException("Too many classes in " + Arguments.MAIN_DEX_LIST_OPTION
475                            + ", main dex capacity exceeded");
476                }
477
478                if (args.minimalMainDex) {
479                    // start second pass directly in a secondary dex file.
480                    createDexFile();
481                }
482
483                // remaining files
484                for (int i = 0; i < fileNames.length; i++) {
485                    if (processOne(fileNames[i], new NotFilter(mainPassFilter))) {
486                        anyFilesProcessed = true;
487                    }
488                }
489            } else {
490                // without --main-dex-list
491                for (int i = 0; i < fileNames.length; i++) {
492                    if (processOne(fileNames[i], ClassPathOpener.acceptAll)) {
493                        anyFilesProcessed = true;
494                    }
495                }
496            }
497        } catch (StopProcessing ex) {
498            /*
499             * Ignore it and just let the error reporting do
500             * their things.
501             */
502        }
503
504        if (args.numThreads > 1) {
505            try {
506                threadPool.shutdown();
507                threadPool.awaitTermination(600L, TimeUnit.SECONDS);
508            } catch (InterruptedException ex) {
509                throw new RuntimeException("Timed out waiting for threads.");
510            }
511        }
512
513        if (errors != 0) {
514            DxConsole.err.println(errors + " error" +
515                    ((errors == 1) ? "" : "s") + "; aborting");
516            return false;
517        }
518
519        if (args.incremental && !anyFilesProcessed) {
520            return true;
521        }
522
523        if (!(anyFilesProcessed || args.emptyOk)) {
524            DxConsole.err.println("no classfiles specified");
525            return false;
526        }
527
528        if (args.optimize && args.statistics) {
529            CodeStatistics.dumpStatistics(DxConsole.out);
530        }
531
532        return true;
533    }
534
535    private static void createDexFile() {
536        if (outputDex != null) {
537            dexOutputArrays.add(writeDex());
538        }
539
540        outputDex = new DexFile(args.dexOptions);
541
542        if (args.dumpWidth != 0) {
543            outputDex.setDumpWidth(args.dumpWidth);
544        }
545    }
546
547    /**
548     * Processes one pathname element.
549     *
550     * @param pathname {@code non-null;} the pathname to process. May
551     * be the path of a class file, a jar file, or a directory
552     * containing class files.
553     * @param filter {@code non-null;} A filter for excluding files.
554     * @return whether any processing actually happened
555     */
556    private static boolean processOne(String pathname, FileNameFilter filter) {
557        ClassPathOpener opener;
558
559        opener = new ClassPathOpener(pathname, false, filter,
560                new ClassPathOpener.Consumer() {
561            public boolean processFileBytes(String name, long lastModified, byte[] bytes) {
562                if (args.numThreads > 1) {
563                    threadPool.execute(new ParallelProcessor(name, lastModified, bytes));
564                    return false;
565                } else {
566                    return Main.processFileBytes(name, lastModified, bytes);
567                }
568            }
569            public void onException(Exception ex) {
570                if (ex instanceof StopProcessing) {
571                    throw (StopProcessing) ex;
572                } else if (ex instanceof SimException) {
573                    DxConsole.err.println("\nEXCEPTION FROM SIMULATION:");
574                    DxConsole.err.println(ex.getMessage() + "\n");
575                    DxConsole.err.println(((SimException) ex).getContext());
576                } else {
577                    DxConsole.err.println("\nUNEXPECTED TOP-LEVEL EXCEPTION:");
578                    ex.printStackTrace(DxConsole.err);
579                }
580                errors++;
581            }
582            public void onProcessArchiveStart(File file) {
583                if (args.verbose) {
584                    DxConsole.out.println("processing archive " + file +
585                            "...");
586                }
587            }
588        });
589
590        return opener.process();
591    }
592
593    /**
594     * Processes one file, which may be either a class or a resource.
595     *
596     * @param name {@code non-null;} name of the file
597     * @param bytes {@code non-null;} contents of the file
598     * @return whether processing was successful
599     */
600    private static boolean processFileBytes(String name, long lastModified, byte[] bytes) {
601        boolean isClass = name.endsWith(".class");
602        boolean isClassesDex = name.equals(DexFormat.DEX_IN_JAR_NAME);
603        boolean keepResources = (outputResources != null);
604
605        if (!isClass && !isClassesDex && !keepResources) {
606            if (args.verbose) {
607                DxConsole.out.println("ignored resource " + name);
608            }
609            return false;
610        }
611
612        if (args.verbose) {
613            DxConsole.out.println("processing " + name + "...");
614        }
615
616        String fixedName = fixPath(name);
617
618        if (isClass) {
619
620            if (keepResources && args.keepClassesInJar) {
621                synchronized (outputResources) {
622                    outputResources.put(fixedName, bytes);
623                }
624            }
625            if (lastModified < minimumFileAge) {
626                return true;
627            }
628            return processClass(fixedName, bytes);
629        } else if (isClassesDex) {
630            synchronized (libraryDexBuffers) {
631                libraryDexBuffers.add(bytes);
632            }
633            return true;
634        } else {
635            synchronized (outputResources) {
636                outputResources.put(fixedName, bytes);
637            }
638            return true;
639        }
640    }
641
642    /**
643     * Processes one classfile.
644     *
645     * @param name {@code non-null;} name of the file, clipped such that it
646     * <i>should</i> correspond to the name of the class it contains
647     * @param bytes {@code non-null;} contents of the file
648     * @return whether processing was successful
649     */
650    private static boolean processClass(String name, byte[] bytes) {
651        if (! args.coreLibrary) {
652            checkClassName(name);
653        }
654
655        DirectClassFile cf =
656            new DirectClassFile(bytes, name, args.cfOptions.strictNameCheck);
657
658        cf.setAttributeFactory(StdAttributeFactory.THE_ONE);
659        cf.getMagic();
660
661        int numMethodIds = outputDex.getMethodIds().items().size();
662        int numFieldIds = outputDex.getFieldIds().items().size();
663        int numTypeIds = outputDex.getTypeIds().items().size();
664        int constantPoolSize = cf.getConstantPool().size();
665
666        if (args.multiDex && ((numMethodIds + constantPoolSize > args.maxNumberOfIdxPerDex) ||
667            (numFieldIds + constantPoolSize > args.maxNumberOfIdxPerDex) ||
668            (numTypeIds + constantPoolSize
669                    /* annotation added by dx are not counted in numTypeIds */
670                    + AnnotationUtils.DALVIK_ANNOTATION_NUMBER
671                    > args.maxNumberOfIdxPerDex))) {
672          createDexFile();
673        }
674
675        try {
676            ClassDefItem clazz =
677                CfTranslator.translate(cf, bytes, args.cfOptions, args.dexOptions, outputDex);
678            synchronized (outputDex) {
679                outputDex.add(clazz);
680            }
681            return true;
682
683        } catch (ParseException ex) {
684            DxConsole.err.println("\ntrouble processing:");
685            if (args.debug) {
686                ex.printStackTrace(DxConsole.err);
687            } else {
688                ex.printContext(DxConsole.err);
689            }
690        }
691        errors++;
692        return false;
693    }
694
695    /**
696     * Check the class name to make sure it's not a "core library"
697     * class. If there is a problem, this updates the error count and
698     * throws an exception to stop processing.
699     *
700     * @param name {@code non-null;} the fully-qualified internal-form
701     * class name
702     */
703    private static void checkClassName(String name) {
704        boolean bogus = false;
705
706        if (name.startsWith("java/")) {
707            bogus = true;
708        } else if (name.startsWith("javax/")) {
709            int slashAt = name.indexOf('/', 6);
710            if (slashAt == -1) {
711                // Top-level javax classes are verboten.
712                bogus = true;
713            } else {
714                String pkg = name.substring(6, slashAt);
715                bogus = (Arrays.binarySearch(JAVAX_CORE, pkg) >= 0);
716            }
717        }
718
719        if (! bogus) {
720            return;
721        }
722
723        /*
724         * The user is probably trying to include an entire desktop
725         * core library in a misguided attempt to get their application
726         * working. Try to help them understand what's happening.
727         */
728
729        DxConsole.err.println("\ntrouble processing \"" + name + "\":\n\n" +
730                IN_RE_CORE_CLASSES);
731        errors++;
732        throw new StopProcessing();
733    }
734
735    /**
736     * Converts {@link #outputDex} into a {@code byte[]} and do whatever
737     * human-oriented dumping is required.
738     *
739     * @return {@code null-ok;} the converted {@code byte[]} or {@code null}
740     * if there was a problem
741     */
742    private static byte[] writeDex() {
743        byte[] outArray = null;
744
745        try {
746            try {
747                if (args.methodToDump != null) {
748                    /*
749                     * Simply dump the requested method. Note: The call
750                     * to toDex() is required just to get the underlying
751                     * structures ready.
752                     */
753                    outputDex.toDex(null, false);
754                    dumpMethod(outputDex, args.methodToDump, humanOutWriter);
755                } else {
756                    /*
757                     * This is the usual case: Create an output .dex file,
758                     * and write it, dump it, etc.
759                     */
760                    outArray = outputDex.toDex(humanOutWriter, args.verboseDump);
761                }
762
763                if (args.statistics) {
764                    DxConsole.out.println(outputDex.getStatistics().toHuman());
765                }
766            } finally {
767                if (humanOutWriter != null) {
768                    humanOutWriter.flush();
769                }
770            }
771        } catch (Exception ex) {
772            if (args.debug) {
773                DxConsole.err.println("\ntrouble writing output:");
774                ex.printStackTrace(DxConsole.err);
775            } else {
776                DxConsole.err.println("\ntrouble writing output: " +
777                                   ex.getMessage());
778            }
779            return null;
780        }
781
782        return outArray;
783    }
784
785    /**
786     * Creates a jar file from the resources (including dex file arrays).
787     *
788     * @param fileName {@code non-null;} name of the file
789     * @return whether the creation was successful
790     */
791    private static boolean createJar(String fileName) {
792        /*
793         * Make or modify the manifest (as appropriate), put the dex
794         * array into the resources map, and then process the entire
795         * resources map in a uniform manner.
796         */
797
798        try {
799            Manifest manifest = makeManifest();
800            OutputStream out = openOutput(fileName);
801            JarOutputStream jarOut = new JarOutputStream(out, manifest);
802
803            try {
804                for (Map.Entry<String, byte[]> e :
805                         outputResources.entrySet()) {
806                    String name = e.getKey();
807                    byte[] contents = e.getValue();
808                    JarEntry entry = new JarEntry(name);
809                    int length = contents.length;
810
811                    if (args.verbose) {
812                        DxConsole.out.println("writing " + name + "; size " + length + "...");
813                    }
814
815                    entry.setSize(length);
816                    jarOut.putNextEntry(entry);
817                    jarOut.write(contents);
818                    jarOut.closeEntry();
819                }
820            } finally {
821                jarOut.finish();
822                jarOut.flush();
823                closeOutput(out);
824            }
825        } catch (Exception ex) {
826            if (args.debug) {
827                DxConsole.err.println("\ntrouble writing output:");
828                ex.printStackTrace(DxConsole.err);
829            } else {
830                DxConsole.err.println("\ntrouble writing output: " +
831                                   ex.getMessage());
832            }
833            return false;
834        }
835
836        return true;
837    }
838
839    /**
840     * Creates and returns the manifest to use for the output. This may
841     * modify {@link #outputResources} (removing the pre-existing manifest).
842     *
843     * @return {@code non-null;} the manifest
844     */
845    private static Manifest makeManifest() throws IOException {
846        byte[] manifestBytes = outputResources.get(MANIFEST_NAME);
847        Manifest manifest;
848        Attributes attribs;
849
850        if (manifestBytes == null) {
851            // We need to construct an entirely new manifest.
852            manifest = new Manifest();
853            attribs = manifest.getMainAttributes();
854            attribs.put(Attributes.Name.MANIFEST_VERSION, "1.0");
855        } else {
856            manifest = new Manifest(new ByteArrayInputStream(manifestBytes));
857            attribs = manifest.getMainAttributes();
858            outputResources.remove(MANIFEST_NAME);
859        }
860
861        String createdBy = attribs.getValue(CREATED_BY);
862        if (createdBy == null) {
863            createdBy = "";
864        } else {
865            createdBy += " + ";
866        }
867        createdBy += "dx " + Version.VERSION;
868
869        attribs.put(CREATED_BY, createdBy);
870        attribs.putValue("Dex-Location", DexFormat.DEX_IN_JAR_NAME);
871
872        return manifest;
873    }
874
875    /**
876     * Opens and returns the named file for writing, treating "-" specially.
877     *
878     * @param name {@code non-null;} the file name
879     * @return {@code non-null;} the opened file
880     */
881    private static OutputStream openOutput(String name) throws IOException {
882        if (name.equals("-") ||
883                name.startsWith("-.")) {
884            return System.out;
885        }
886
887        return new FileOutputStream(name);
888    }
889
890    /**
891     * Flushes and closes the given output stream, except if it happens to be
892     * {@link System#out} in which case this method does the flush but not
893     * the close. This method will also silently do nothing if given a
894     * {@code null} argument.
895     *
896     * @param stream {@code null-ok;} what to close
897     */
898    private static void closeOutput(OutputStream stream) throws IOException {
899        if (stream == null) {
900            return;
901        }
902
903        stream.flush();
904
905        if (stream != System.out) {
906            stream.close();
907        }
908    }
909
910    /**
911     * Returns the "fixed" version of a given file path, suitable for
912     * use as a path within a {@code .jar} file and for checking
913     * against a classfile-internal "this class" name. This looks for
914     * the last instance of the substring {@code "/./"} within
915     * the path, and if it finds it, it takes the portion after to be
916     * the fixed path. If that isn't found but the path starts with
917     * {@code "./"}, then that prefix is removed and the rest is
918     * return. If neither of these is the case, this method returns
919     * its argument.
920     *
921     * @param path {@code non-null;} the path to "fix"
922     * @return {@code non-null;} the fixed version (which might be the same as
923     * the given {@code path})
924     */
925    private static String fixPath(String path) {
926        /*
927         * If the path separator is \ (like on windows), we convert the
928         * path to a standard '/' separated path.
929         */
930        if (File.separatorChar == '\\') {
931            path = path.replace('\\', '/');
932        }
933
934        int index = path.lastIndexOf("/./");
935
936        if (index != -1) {
937            return path.substring(index + 3);
938        }
939
940        if (path.startsWith("./")) {
941            return path.substring(2);
942        }
943
944        return path;
945    }
946
947    /**
948     * Dumps any method with the given name in the given file.
949     *
950     * @param dex {@code non-null;} the dex file
951     * @param fqName {@code non-null;} the fully-qualified name of the
952     * method(s)
953     * @param out {@code non-null;} where to dump to
954     */
955    private static void dumpMethod(DexFile dex, String fqName,
956            OutputStreamWriter out) {
957        boolean wildcard = fqName.endsWith("*");
958        int lastDot = fqName.lastIndexOf('.');
959
960        if ((lastDot <= 0) || (lastDot == (fqName.length() - 1))) {
961            DxConsole.err.println("bogus fully-qualified method name: " +
962                               fqName);
963            return;
964        }
965
966        String className = fqName.substring(0, lastDot).replace('.', '/');
967        String methodName = fqName.substring(lastDot + 1);
968        ClassDefItem clazz = dex.getClassOrNull(className);
969
970        if (clazz == null) {
971            DxConsole.err.println("no such class: " + className);
972            return;
973        }
974
975        if (wildcard) {
976            methodName = methodName.substring(0, methodName.length() - 1);
977        }
978
979        ArrayList<EncodedMethod> allMeths = clazz.getMethods();
980        TreeMap<CstNat, EncodedMethod> meths =
981            new TreeMap<CstNat, EncodedMethod>();
982
983        /*
984         * Figure out which methods to include in the output, and get them
985         * all sorted, so that the printout code is robust with respect to
986         * changes in the underlying order.
987         */
988        for (EncodedMethod meth : allMeths) {
989            String methName = meth.getName().getString();
990            if ((wildcard && methName.startsWith(methodName)) ||
991                (!wildcard && methName.equals(methodName))) {
992                meths.put(meth.getRef().getNat(), meth);
993            }
994        }
995
996        if (meths.size() == 0) {
997            DxConsole.err.println("no such method: " + fqName);
998            return;
999        }
1000
1001        PrintWriter pw = new PrintWriter(out);
1002
1003        for (EncodedMethod meth : meths.values()) {
1004            // TODO: Better stuff goes here, perhaps.
1005            meth.debugPrint(pw, args.verboseDump);
1006
1007            /*
1008             * The (default) source file is an attribute of the class, but
1009             * it's useful to see it in method dumps.
1010             */
1011            CstString sourceFile = clazz.getSourceFile();
1012            if (sourceFile != null) {
1013                pw.println("  source file: " + sourceFile.toQuoted());
1014            }
1015
1016            Annotations methodAnnotations =
1017                clazz.getMethodAnnotations(meth.getRef());
1018            AnnotationsList parameterAnnotations =
1019                clazz.getParameterAnnotations(meth.getRef());
1020
1021            if (methodAnnotations != null) {
1022                pw.println("  method annotations:");
1023                for (Annotation a : methodAnnotations.getAnnotations()) {
1024                    pw.println("    " + a);
1025                }
1026            }
1027
1028            if (parameterAnnotations != null) {
1029                pw.println("  parameter annotations:");
1030                int sz = parameterAnnotations.size();
1031                for (int i = 0; i < sz; i++) {
1032                    pw.println("    parameter " + i);
1033                    Annotations annotations = parameterAnnotations.get(i);
1034                    for (Annotation a : annotations.getAnnotations()) {
1035                        pw.println("      " + a);
1036                    }
1037                }
1038            }
1039        }
1040
1041        pw.flush();
1042    }
1043
1044    private static class NotFilter implements FileNameFilter {
1045        private final FileNameFilter filter;
1046
1047        private NotFilter(FileNameFilter filter) {
1048            this.filter = filter;
1049        }
1050
1051        @Override
1052        public boolean accept(String path) {
1053            return !filter.accept(path);
1054        }
1055    }
1056
1057    /**
1058     * A quick and accurate filter for when file path can be trusted.
1059     */
1060    private static class MainDexListFilter implements FileNameFilter {
1061
1062        @Override
1063        public boolean accept(String fullPath) {
1064            if (fullPath.endsWith(".class")) {
1065                String path = fixPath(fullPath);
1066                return classesInMainDex.contains(path);
1067            } else {
1068                return true;
1069            }
1070        }
1071    }
1072
1073    /**
1074     * A best effort conservative filter for when file path can <b>not</b> be trusted.
1075     */
1076    private static class BestEffortMainDexListFilter implements FileNameFilter {
1077
1078       Map<String, List<String>> map = new HashMap<String, List<String>>();
1079
1080       public BestEffortMainDexListFilter() {
1081           for (String pathOfClass : classesInMainDex) {
1082               String normalized = fixPath(pathOfClass);
1083               String simple = getSimpleName(normalized);
1084               List<String> fullPath = map.get(simple);
1085               if (fullPath == null) {
1086                   fullPath = new ArrayList<String>(1);
1087                   map.put(simple, fullPath);
1088               }
1089               fullPath.add(normalized);
1090           }
1091        }
1092
1093        @Override
1094        public boolean accept(String path) {
1095            if (path.endsWith(".class")) {
1096                String normalized = fixPath(path);
1097                String simple = getSimpleName(normalized);
1098                List<String> fullPaths = map.get(simple);
1099                if (fullPaths != null) {
1100                    for (String fullPath : fullPaths) {
1101                        if (normalized.endsWith(fullPath)) {
1102                            return true;
1103                        }
1104                    }
1105                }
1106            }
1107            return false;
1108        }
1109
1110        private static String getSimpleName(String path) {
1111            int index = path.lastIndexOf('/');
1112            if (index >= 0) {
1113                return path.substring(index + 1);
1114            } else {
1115                return path;
1116            }
1117        }
1118    }
1119
1120    /**
1121     * Exception class used to halt processing prematurely.
1122     */
1123    private static class StopProcessing extends RuntimeException {
1124        // This space intentionally left blank.
1125    }
1126
1127    /**
1128     * Command-line argument parser and access.
1129     */
1130    public static class Arguments {
1131
1132        private static final String MINIMAL_MAIN_DEX_OPTION = "--minimal-main-dex";
1133
1134        private static final String MAIN_DEX_LIST_OPTION = "--main-dex-list";
1135
1136        private static final String MULTI_DEX_OPTION = "--multi-dex";
1137
1138        private static final String NUM_THREADS_OPTION = "--num-threads";
1139
1140        private static final String INCREMENTAL_OPTION = "--incremental";
1141
1142        /** whether to run in debug mode */
1143        public boolean debug = false;
1144
1145        /** whether to emit high-level verbose human-oriented output */
1146        public boolean verbose = false;
1147
1148        /** whether to emit verbose human-oriented output in the dump file */
1149        public boolean verboseDump = false;
1150
1151        /** whether we are constructing a core library */
1152        public boolean coreLibrary = false;
1153
1154        /** {@code null-ok;} particular method to dump */
1155        public String methodToDump = null;
1156
1157        /** max width for columnar output */
1158        public int dumpWidth = 0;
1159
1160        /** {@code null-ok;} output file name for binary file */
1161        public String outName = null;
1162
1163        /** {@code null-ok;} output file name for human-oriented dump */
1164        public String humanOutName = null;
1165
1166        /** whether strict file-name-vs-class-name checking should be done */
1167        public boolean strictNameCheck = true;
1168
1169        /**
1170         * whether it is okay for there to be no {@code .class} files
1171         * to process
1172         */
1173        public boolean emptyOk = false;
1174
1175        /**
1176         * whether the binary output is to be a {@code .jar} file
1177         * instead of a plain {@code .dex}
1178         */
1179        public boolean jarOutput = false;
1180
1181        /**
1182         * when writing a {@code .jar} file, whether to still
1183         * keep the {@code .class} files
1184         */
1185        public boolean keepClassesInJar = false;
1186
1187        /** what API level to target */
1188        public int targetApiLevel = DexFormat.API_NO_EXTENDED_OPCODES;
1189
1190        /** how much source position info to preserve */
1191        public int positionInfo = PositionList.LINES;
1192
1193        /** whether to keep local variable information */
1194        public boolean localInfo = true;
1195
1196        /** whether to merge with the output dex file if it exists. */
1197        public boolean incremental = false;
1198
1199        /** whether to force generation of const-string/jumbo for all indexes,
1200         *  to allow merges between dex files with many strings. */
1201        public boolean forceJumbo = false;
1202
1203        /** {@code non-null} after {@link #parse}; file name arguments */
1204        public String[] fileNames;
1205
1206        /** whether to do SSA/register optimization */
1207        public boolean optimize = true;
1208
1209        /** Filename containg list of methods to optimize */
1210        public String optimizeListFile = null;
1211
1212        /** Filename containing list of methods to NOT optimize */
1213        public String dontOptimizeListFile = null;
1214
1215        /** Whether to print statistics to stdout at end of compile cycle */
1216        public boolean statistics;
1217
1218        /** Options for class file transformation */
1219        public CfOptions cfOptions;
1220
1221        /** Options for dex file output */
1222        public DexOptions dexOptions;
1223
1224        /** number of threads to run with */
1225        public int numThreads = 1;
1226
1227        /** generation of multiple dex is allowed */
1228        public boolean multiDex = false;
1229
1230        /** Optional file containing a list of class files containing classes to be forced in main
1231         * dex */
1232        public String mainDexListFile = null;
1233
1234        /** Produce the smallest possible main dex. Ignored unless multiDex is true and
1235         * mainDexListFile is specified and non empty. */
1236        public boolean minimalMainDex = false;
1237
1238        private int maxNumberOfIdxPerDex = DexFormat.MAX_MEMBER_IDX + 1;
1239
1240        private static class ArgumentsParser {
1241
1242            /** The arguments to process. */
1243            private final String[] arguments;
1244            /** The index of the next argument to process. */
1245            private int index;
1246            /** The current argument being processed after a {@link #getNext()} call. */
1247            private String current;
1248            /** The last value of an argument processed by {@link #isArg(String)}. */
1249            private String lastValue;
1250
1251            public ArgumentsParser(String[] arguments) {
1252                this.arguments = arguments;
1253                index = 0;
1254            }
1255
1256            public String getCurrent() {
1257                return current;
1258            }
1259
1260            public String getLastValue() {
1261                return lastValue;
1262            }
1263
1264            /**
1265             * Moves on to the next argument.
1266             * Returns false when we ran out of arguments that start with --.
1267             */
1268            public boolean getNext() {
1269                if (index >= arguments.length) {
1270                    return false;
1271                }
1272                current = arguments[index];
1273                if (current.equals("--") || !current.startsWith("--")) {
1274                    return false;
1275                }
1276                index++;
1277                return true;
1278            }
1279
1280            /**
1281             * Similar to {@link #getNext()}, this moves on the to next argument.
1282             * It does not check however whether the argument starts with --
1283             * and thus can be used to retrieve values.
1284             */
1285            private boolean getNextValue() {
1286                if (index >= arguments.length) {
1287                    return false;
1288                }
1289                current = arguments[index];
1290                index++;
1291                return true;
1292            }
1293
1294            /**
1295             * Returns all the arguments that have not been processed yet.
1296             */
1297            public String[] getRemaining() {
1298                int n = arguments.length - index;
1299                String[] remaining = new String[n];
1300                if (n > 0) {
1301                    System.arraycopy(arguments, index, remaining, 0, n);
1302                }
1303                return remaining;
1304            }
1305
1306            /**
1307             * Checks the current argument against the given prefix.
1308             * If prefix is in the form '--name=', an extra value is expected.
1309             * The argument can then be in the form '--name=value' or as a 2-argument
1310             * form '--name value'.
1311             */
1312            public boolean isArg(String prefix) {
1313                int n = prefix.length();
1314                if (n > 0 && prefix.charAt(n-1) == '=') {
1315                    // Argument accepts a value. Capture it.
1316                    if (current.startsWith(prefix)) {
1317                        // Argument is in the form --name=value, split the value out
1318                        lastValue = current.substring(n);
1319                        return true;
1320                    } else {
1321                        // Check whether we have "--name value" as 2 arguments
1322                        prefix = prefix.substring(0, n-1);
1323                        if (current.equals(prefix)) {
1324                            if (getNextValue()) {
1325                                lastValue = current;
1326                                return true;
1327                            } else {
1328                                System.err.println("Missing value after parameter " + prefix);
1329                                throw new UsageException();
1330                            }
1331                        }
1332                        return false;
1333                    }
1334                } else {
1335                    // Argument does not accept a value.
1336                    return current.equals(prefix);
1337                }
1338            }
1339        }
1340
1341        /**
1342         * Parses the given command-line arguments.
1343         *
1344         * @param args {@code non-null;} the arguments
1345         */
1346        public void parse(String[] args) {
1347            ArgumentsParser parser = new ArgumentsParser(args);
1348
1349            boolean outputIsDirectory = false;
1350            boolean outputIsDirectDex = false;
1351
1352            while(parser.getNext()) {
1353                if (parser.isArg("--debug")) {
1354                    debug = true;
1355                } else if (parser.isArg("--verbose")) {
1356                    verbose = true;
1357                } else if (parser.isArg("--verbose-dump")) {
1358                    verboseDump = true;
1359                } else if (parser.isArg("--no-files")) {
1360                    emptyOk = true;
1361                } else if (parser.isArg("--no-optimize")) {
1362                    optimize = false;
1363                } else if (parser.isArg("--no-strict")) {
1364                    strictNameCheck = false;
1365                } else if (parser.isArg("--core-library")) {
1366                    coreLibrary = true;
1367                } else if (parser.isArg("--statistics")) {
1368                    statistics = true;
1369                } else if (parser.isArg("--optimize-list=")) {
1370                    if (dontOptimizeListFile != null) {
1371                        System.err.println("--optimize-list and "
1372                                + "--no-optimize-list are incompatible.");
1373                        throw new UsageException();
1374                    }
1375                    optimize = true;
1376                    optimizeListFile = parser.getLastValue();
1377                } else if (parser.isArg("--no-optimize-list=")) {
1378                    if (dontOptimizeListFile != null) {
1379                        System.err.println("--optimize-list and "
1380                                + "--no-optimize-list are incompatible.");
1381                        throw new UsageException();
1382                    }
1383                    optimize = true;
1384                    dontOptimizeListFile = parser.getLastValue();
1385                } else if (parser.isArg("--keep-classes")) {
1386                    keepClassesInJar = true;
1387                } else if (parser.isArg("--output=")) {
1388                    outName = parser.getLastValue();
1389                    if (new File(outName).isDirectory()) {
1390                        jarOutput = false;
1391                        outputIsDirectory = true;
1392                    } else if (FileUtils.hasArchiveSuffix(outName)) {
1393                        jarOutput = true;
1394                    } else if (outName.endsWith(".dex") ||
1395                               outName.equals("-")) {
1396                        jarOutput = false;
1397                        outputIsDirectDex = true;
1398                    } else {
1399                        System.err.println("unknown output extension: " +
1400                                           outName);
1401                        throw new UsageException();
1402                    }
1403                } else if (parser.isArg("--dump-to=")) {
1404                    humanOutName = parser.getLastValue();
1405                } else if (parser.isArg("--dump-width=")) {
1406                    dumpWidth = Integer.parseInt(parser.getLastValue());
1407                } else if (parser.isArg("--dump-method=")) {
1408                    methodToDump = parser.getLastValue();
1409                    jarOutput = false;
1410                } else if (parser.isArg("--positions=")) {
1411                    String pstr = parser.getLastValue().intern();
1412                    if (pstr == "none") {
1413                        positionInfo = PositionList.NONE;
1414                    } else if (pstr == "important") {
1415                        positionInfo = PositionList.IMPORTANT;
1416                    } else if (pstr == "lines") {
1417                        positionInfo = PositionList.LINES;
1418                    } else {
1419                        System.err.println("unknown positions option: " +
1420                                           pstr);
1421                        throw new UsageException();
1422                    }
1423                } else if (parser.isArg("--no-locals")) {
1424                    localInfo = false;
1425                } else if (parser.isArg(NUM_THREADS_OPTION + "=")) {
1426                    numThreads = Integer.parseInt(parser.getLastValue());
1427                } else if (parser.isArg(INCREMENTAL_OPTION)) {
1428                    incremental = true;
1429                } else if (parser.isArg("--force-jumbo")) {
1430                    forceJumbo = true;
1431                } else if (parser.isArg(MULTI_DEX_OPTION)) {
1432                    multiDex = true;
1433                } else if (parser.isArg(MAIN_DEX_LIST_OPTION + "=")) {
1434                    mainDexListFile = parser.getLastValue();
1435                } else if (parser.isArg(MINIMAL_MAIN_DEX_OPTION)) {
1436                    minimalMainDex = true;
1437                } else if (parser.isArg("--set-max-idx-number=")) { // undocumented test option
1438                    maxNumberOfIdxPerDex = Integer.parseInt(parser.getLastValue());
1439              } else {
1440                    System.err.println("unknown option: " + parser.getCurrent());
1441                    throw new UsageException();
1442                }
1443            }
1444
1445            fileNames = parser.getRemaining();
1446            if (fileNames.length == 0) {
1447                if (!emptyOk) {
1448                    System.err.println("no input files specified");
1449                    throw new UsageException();
1450                }
1451            } else if (emptyOk) {
1452                System.out.println("ignoring input files");
1453            }
1454
1455            if ((humanOutName == null) && (methodToDump != null)) {
1456                humanOutName = "-";
1457            }
1458
1459            if (mainDexListFile != null && !multiDex) {
1460                System.err.println(MAIN_DEX_LIST_OPTION + " is only supported in combination with "
1461                    + MULTI_DEX_OPTION);
1462                throw new UsageException();
1463            }
1464
1465            if (minimalMainDex && (mainDexListFile == null || !multiDex)) {
1466                System.err.println(MINIMAL_MAIN_DEX_OPTION + " is only supported in combination with "
1467                    + MULTI_DEX_OPTION + " and " + MAIN_DEX_LIST_OPTION);
1468                throw new UsageException();
1469            }
1470
1471            if (multiDex && numThreads != 1) {
1472                System.out.println(NUM_THREADS_OPTION + "is ignored when used with "
1473                    + MULTI_DEX_OPTION);
1474            }
1475
1476            if (multiDex && incremental) {
1477                System.err.println(INCREMENTAL_OPTION + " is not supported with "
1478                    + MULTI_DEX_OPTION);
1479                throw new UsageException();
1480            }
1481
1482            if (multiDex && outputIsDirectDex) {
1483                System.err.println("Unsupported output \"" + outName +"\". " + MULTI_DEX_OPTION +
1484                        " supports only archive or directory output");
1485                throw new UsageException();
1486            }
1487
1488            if (outputIsDirectory && !multiDex) {
1489                outName = new File(outName, DexFormat.DEX_IN_JAR_NAME).getPath();
1490            }
1491
1492            makeOptionsObjects();
1493        }
1494
1495        /**
1496         * Copies relevent arguments over into CfOptions and
1497         * DexOptions instances.
1498         */
1499        private void makeOptionsObjects() {
1500            cfOptions = new CfOptions();
1501            cfOptions.positionInfo = positionInfo;
1502            cfOptions.localInfo = localInfo;
1503            cfOptions.strictNameCheck = strictNameCheck;
1504            cfOptions.optimize = optimize;
1505            cfOptions.optimizeListFile = optimizeListFile;
1506            cfOptions.dontOptimizeListFile = dontOptimizeListFile;
1507            cfOptions.statistics = statistics;
1508            cfOptions.warn = DxConsole.err;
1509
1510            dexOptions = new DexOptions();
1511            dexOptions.targetApiLevel = targetApiLevel;
1512            dexOptions.forceJumbo = forceJumbo;
1513        }
1514    }
1515
1516    /** Runnable helper class to process files in multiple threads */
1517    private static class ParallelProcessor implements Runnable {
1518
1519        String path;
1520        long lastModified;
1521        byte[] bytes;
1522
1523        /**
1524         * Constructs an instance.
1525         *
1526         * @param path {@code non-null;} filename of element. May not be a valid
1527         * filesystem path.
1528         * @param bytes {@code non-null;} file data
1529         */
1530        private ParallelProcessor(String path, long lastModified, byte bytes[]) {
1531            this.path = path;
1532            this.lastModified = lastModified;
1533            this.bytes = bytes;
1534        }
1535
1536        /**
1537         * Task run by each thread in the thread pool. Runs processFileBytes
1538         * with the given path and bytes.
1539         */
1540        public void run() {
1541            if (Main.processFileBytes(path, lastModified, bytes)) {
1542                anyFilesProcessed = true;
1543            }
1544        }
1545    }
1546}
1547