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