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