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