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