diff options
author | Ben Gruver <bgruv@google.com> | 2017-05-04 21:15:00 +0000 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2017-05-04 21:15:00 +0000 |
commit | f9202b3a88389bdd0cc79cdf0c37fa1219574729 (patch) | |
tree | 89a4e319452bb5b95e307dfac87fa710e2675c74 | |
parent | db31e77294b8be6b55bbf4dc46abb37e79fd8f4a (diff) | |
parent | d9a6252d372ff9b2aae2052646164e2d240bf408 (diff) | |
download | smali-f9202b3a88389bdd0cc79cdf0c37fa1219574729.tar.gz |
Merge remote-tracking branch 'aosp/upstream-master' am: 8764034eaf
am: d9a6252d37
Change-Id: Ie82c86673e9cec310e6ad45e02ea61ceb3d8ff6b
162 files changed, 8528 insertions, 3409 deletions
@@ -2,7 +2,7 @@ smali/baksmali is an assembler/disassembler for the dex format used by dalvik, Android's Java VM implementation. The syntax is loosely based on Jasmin's/dedexer's syntax, and supports the full functionality of the dex format (annotations, debug info, line info, etc.) -Downloads are at https://bitbucket.org/JesusFreke/smali/downloads. If you are interested in submitting a patch, feel free to send me a pull request here. +Downloads are at https://bitbucket.org/JesusFreke/smali/downloads/. If you are interested in submitting a patch, feel free to send me a pull request here. See [the wiki](https://github.com/JesusFreke/smali/wiki) for more info/news/release notes/etc. diff --git a/baksmali/build.gradle b/baksmali/build.gradle index f3a14b19..aae88a35 100644 --- a/baksmali/build.gradle +++ b/baksmali/build.gradle @@ -41,8 +41,8 @@ buildscript { dependencies { compile project(':util') compile project(':dexlib2') - compile depends.commons_cli compile depends.guava + compile depends.jcommander testCompile depends.junit testCompile project(':smali') @@ -59,7 +59,7 @@ task fatJar(type: Jar) { classifier = 'fat' manifest { - attributes('Main-Class': 'org.jf.baksmali.main') + attributes('Main-Class': 'org.jf.baksmali.Main') } doLast { @@ -92,7 +92,9 @@ task proguard(type: proguard.gradle.ProGuardTask, dependsOn: fatJar) { dontobfuscate dontoptimize - keep 'public class org.jf.baksmali.main { public static void main(java.lang.String[]); }' + keep 'public class org.jf.baksmali.Main { public static void main(java.lang.String[]); }' + keep 'public class org.jf.util.jcommander.ColonParameterSplitter' + keep 'class com.beust.jcommander.** { *; }' keepclassmembers 'enum * { public static **[] values(); public static ** valueOf(java.lang.String); }' dontwarn 'com.google.common.**' @@ -100,3 +102,17 @@ task proguard(type: proguard.gradle.ProGuardTask, dependsOn: fatJar) { } tasks.getByPath(':release').dependsOn(proguard) + +task fastbuild(dependsOn: build) { +} + +task fb(dependsOn: fastbuild) { +} + +tasks.getByPath('javadoc').onlyIf({ + !gradle.taskGraph.hasTask(fastbuild) +}) + +tasks.getByPath('test').onlyIf({ + !gradle.taskGraph.hasTask(fastbuild) +})
\ No newline at end of file diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/CatchMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/CatchMethodItem.java index 6c67d4ac..4b545ee6 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/CatchMethodItem.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/CatchMethodItem.java @@ -28,7 +28,7 @@ package org.jf.baksmali.Adaptors; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.util.IndentingWriter; import javax.annotation.Nonnull; @@ -42,7 +42,7 @@ public class CatchMethodItem extends MethodItem { private final LabelMethodItem tryEndLabel; private final LabelMethodItem handlerLabel; - public CatchMethodItem(@Nonnull baksmaliOptions options, @Nonnull MethodDefinition.LabelCache labelCache, + public CatchMethodItem(@Nonnull BaksmaliOptions options, @Nonnull MethodDefinition.LabelCache labelCache, int codeAddress, @Nullable String exceptionType, int startAddress, int endAddress, int handlerAddress) { super(codeAddress); diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/ClassDefinition.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/ClassDefinition.java index 2529af8a..361826da 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/ClassDefinition.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/ClassDefinition.java @@ -28,8 +28,7 @@ package org.jf.baksmali.Adaptors; -import com.google.common.collect.Lists; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.dexlib2.AccessFlags; import org.jf.dexlib2.dexbacked.DexBackedClassDef; import org.jf.dexlib2.dexbacked.DexBackedDexFile.InvalidItemIndex; @@ -46,16 +45,16 @@ import java.io.IOException; import java.util.*; public class ClassDefinition { - @Nonnull public final baksmaliOptions options; + @Nonnull public final BaksmaliOptions options; @Nonnull public final ClassDef classDef; @Nonnull private final HashSet<String> fieldsSetInStaticConstructor; protected boolean validationErrors; - public ClassDefinition(@Nonnull baksmaliOptions options, @Nonnull ClassDef classDef) { + public ClassDefinition(@Nonnull BaksmaliOptions options, @Nonnull ClassDef classDef) { this.options = options; this.classDef = classDef; - fieldsSetInStaticConstructor = findFieldsSetInStaticConstructor(); + fieldsSetInStaticConstructor = findFieldsSetInStaticConstructor(classDef); } public boolean hadValidationErrors() { @@ -63,7 +62,7 @@ public class ClassDefinition { } @Nonnull - private HashSet<String> findFieldsSetInStaticConstructor() { + private static HashSet<String> findFieldsSetInStaticConstructor(@Nonnull ClassDef classDef) { HashSet<String> fieldsSetInStaticConstructor = new HashSet<String>(); for (Method method: classDef.getDirectMethods()) { @@ -166,7 +165,7 @@ public class ClassDefinition { writer.write("# annotations\n"); String containingClass = null; - if (options.useImplicitReferences) { + if (options.implicitReferences) { containingClass = classDef.getType(); } diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/EndTryLabelMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/EndTryLabelMethodItem.java index aed315d7..26807048 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/EndTryLabelMethodItem.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/EndTryLabelMethodItem.java @@ -28,14 +28,14 @@ package org.jf.baksmali.Adaptors; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import javax.annotation.Nonnull; public class EndTryLabelMethodItem extends LabelMethodItem { private int endTryAddress; - public EndTryLabelMethodItem(@Nonnull baksmaliOptions options, int codeAddress, int endTryAddress) { + public EndTryLabelMethodItem(@Nonnull BaksmaliOptions options, int codeAddress, int endTryAddress) { super(options, codeAddress, "try_end_"); this.endTryAddress = endTryAddress; } diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/FieldDefinition.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/FieldDefinition.java index ae017914..90291b79 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/FieldDefinition.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/FieldDefinition.java @@ -29,7 +29,7 @@ package org.jf.baksmali.Adaptors; import org.jf.baksmali.Adaptors.EncodedValue.EncodedValueAdaptor; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.dexlib2.AccessFlags; import org.jf.dexlib2.iface.Annotation; import org.jf.dexlib2.iface.Field; @@ -41,7 +41,7 @@ import java.io.IOException; import java.util.Collection; public class FieldDefinition { - public static void writeTo(baksmaliOptions options, IndentingWriter writer, Field field, + public static void writeTo(BaksmaliOptions options, IndentingWriter writer, Field field, boolean setInStaticConstructor) throws IOException { EncodedValue initialValue = field.getInitialValue(); int accessFlags = field.getAccessFlags(); @@ -68,7 +68,7 @@ public class FieldDefinition { writer.write(" = "); String containingClass = null; - if (options.useImplicitReferences) { + if (options.implicitReferences) { containingClass = field.getDefiningClass(); } @@ -82,7 +82,7 @@ public class FieldDefinition { writer.indent(4); String containingClass = null; - if (options.useImplicitReferences) { + if (options.implicitReferences) { containingClass = field.getDefiningClass(); } diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/InstructionMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/InstructionMethodItem.java index fe85fe00..d58b2b68 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/InstructionMethodItem.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/InstructionMethodItem.java @@ -32,7 +32,7 @@ import org.jf.baksmali.Adaptors.MethodDefinition; import org.jf.baksmali.Adaptors.MethodDefinition.InvalidSwitchPayload; import org.jf.baksmali.Adaptors.MethodItem; import org.jf.baksmali.Renderers.LongRenderer; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.dexlib2.Opcode; import org.jf.dexlib2.ReferenceType; import org.jf.dexlib2.VerificationError; @@ -67,7 +67,7 @@ public class InstructionMethodItem<T extends Instruction> extends MethodItem { } private boolean isAllowedOdex(@Nonnull Opcode opcode) { - baksmaliOptions options = methodDef.classDef.options; + BaksmaliOptions options = methodDef.classDef.options; if (options.allowOdex) { return true; } @@ -110,7 +110,7 @@ public class InstructionMethodItem<T extends Instruction> extends MethodItem { if (instruction instanceof ReferenceInstruction) { ReferenceInstruction referenceInstruction = (ReferenceInstruction)instruction; String classContext = null; - if (methodDef.classDef.options.useImplicitReferences) { + if (methodDef.classDef.options.implicitReferences) { classContext = methodDef.method.getDefiningClass(); } diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/OffsetInstructionFormatMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/OffsetInstructionFormatMethodItem.java index 3ffb4bd4..be76edfe 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/OffsetInstructionFormatMethodItem.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/OffsetInstructionFormatMethodItem.java @@ -30,7 +30,7 @@ package org.jf.baksmali.Adaptors.Format; import org.jf.baksmali.Adaptors.LabelMethodItem; import org.jf.baksmali.Adaptors.MethodDefinition; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.dexlib2.Opcode; import org.jf.dexlib2.iface.instruction.OffsetInstruction; import org.jf.util.IndentingWriter; @@ -41,7 +41,7 @@ import java.io.IOException; public class OffsetInstructionFormatMethodItem extends InstructionMethodItem<OffsetInstruction> { protected LabelMethodItem label; - public OffsetInstructionFormatMethodItem(@Nonnull baksmaliOptions options, @Nonnull MethodDefinition methodDef, + public OffsetInstructionFormatMethodItem(@Nonnull BaksmaliOptions options, @Nonnull MethodDefinition methodDef, int codeAddress, OffsetInstruction instruction) { super(methodDef, codeAddress, instruction); diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/LabelMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/LabelMethodItem.java index b152bb69..268d643c 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/LabelMethodItem.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/LabelMethodItem.java @@ -28,18 +28,18 @@ package org.jf.baksmali.Adaptors; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.util.IndentingWriter; import javax.annotation.Nonnull; import java.io.IOException; public class LabelMethodItem extends MethodItem { - private final baksmaliOptions options; + private final BaksmaliOptions options; private final String labelPrefix; private int labelSequence; - public LabelMethodItem(@Nonnull baksmaliOptions options, int codeAddress, @Nonnull String labelPrefix) { + public LabelMethodItem(@Nonnull BaksmaliOptions options, int codeAddress, @Nonnull String labelPrefix) { super(codeAddress); this.options = options; this.labelPrefix = labelPrefix; @@ -76,7 +76,7 @@ public class LabelMethodItem extends MethodItem { public boolean writeTo(IndentingWriter writer) throws IOException { writer.write(':'); writer.write(labelPrefix); - if (options.useSequentialLabels) { + if (options.sequentialLabels) { writer.printUnsignedLongAsHex(labelSequence); } else { writer.printUnsignedLongAsHex(this.getLabelAddress()); diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java index ef2110a8..8161fe49 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java @@ -32,7 +32,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.jf.baksmali.Adaptors.Debug.DebugMethodItem; import org.jf.baksmali.Adaptors.Format.InstructionMethodItemFactory; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.dexlib2.AccessFlags; import org.jf.dexlib2.Format; import org.jf.dexlib2.Opcode; @@ -163,7 +163,7 @@ public class MethodDefinition { } public static void writeEmptyMethodTo(IndentingWriter writer, Method method, - baksmaliOptions options) throws IOException { + BaksmaliOptions options) throws IOException { writer.write(".method "); writeAccessFlags(writer, method.getAccessFlags()); writer.write(method.getName()); @@ -180,7 +180,7 @@ public class MethodDefinition { writeParameters(writer, method, methodParameters, options); String containingClass = null; - if (options.useImplicitReferences) { + if (options.implicitReferences) { containingClass = method.getDefiningClass(); } AnnotationFormatter.writeTo(writer, method.getAnnotations(), containingClass); @@ -212,7 +212,7 @@ public class MethodDefinition { writer.write('\n'); writer.indent(4); - if (classDef.options.useLocalsDirective) { + if (classDef.options.localsDirective) { writer.write(".locals "); writer.printSignedIntAsDec(methodImpl.getRegisterCount() - parameterRegisterCount); } else { @@ -228,7 +228,7 @@ public class MethodDefinition { } String containingClass = null; - if (classDef.options.useImplicitReferences) { + if (classDef.options.implicitReferences) { containingClass = method.getDefiningClass(); } AnnotationFormatter.writeTo(writer, method.getAnnotations(), containingClass); @@ -313,18 +313,18 @@ public class MethodDefinition { private static void writeParameters(IndentingWriter writer, Method method, List<? extends MethodParameter> parameters, - baksmaliOptions options) throws IOException { + BaksmaliOptions options) throws IOException { boolean isStatic = AccessFlags.STATIC.isSet(method.getAccessFlags()); int registerNumber = isStatic?0:1; for (MethodParameter parameter: parameters) { String parameterType = parameter.getType(); String parameterName = parameter.getName(); Collection<? extends Annotation> annotations = parameter.getAnnotations(); - if ((options.outputDebugInfo && parameterName != null) || annotations.size() != 0) { + if ((options.debugInfo && parameterName != null) || annotations.size() != 0) { writer.write(".param p"); writer.printSignedIntAsDec(registerNumber); - if (parameterName != null && options.outputDebugInfo) { + if (parameterName != null && options.debugInfo) { writer.write(", "); ReferenceFormatter.writeStringReference(writer, parameterName); } @@ -335,7 +335,7 @@ public class MethodDefinition { writer.indent(4); String containingClass = null; - if (options.useImplicitReferences) { + if (options.implicitReferences) { containingClass = method.getDefiningClass(); } AnnotationFormatter.writeTo(writer, annotations, containingClass); @@ -374,11 +374,11 @@ public class MethodDefinition { } addTries(methodItems); - if (classDef.options.outputDebugInfo) { + if (classDef.options.debugInfo) { addDebugInfo(methodItems); } - if (classDef.options.useSequentialLabels) { + if (classDef.options.sequentialLabels) { setLabelSequentialNumbers(); } @@ -415,7 +415,7 @@ public class MethodDefinition { methodItems.add(new BlankMethodItem(currentCodeAddress)); } - if (classDef.options.addCodeOffsets) { + if (classDef.options.codeOffsets) { methodItems.add(new MethodItem(currentCodeAddress) { @Override @@ -432,7 +432,8 @@ public class MethodDefinition { }); } - if (!classDef.options.noAccessorComments && (instruction instanceof ReferenceInstruction)) { + if (classDef.options.accessorComments && classDef.options.syntheticAccessorResolver != null && + (instruction instanceof ReferenceInstruction)) { Opcode opcode = instruction.getOpcode(); if (opcode.referenceType == ReferenceType.METHOD) { @@ -493,7 +494,7 @@ public class MethodDefinition { methodItems.add(new BlankMethodItem(currentCodeAddress)); } - if (classDef.options.addCodeOffsets) { + if (classDef.options.codeOffsets) { methodItems.add(new MethodItem(currentCodeAddress) { @Override @@ -597,7 +598,7 @@ public class MethodDefinition { @Nullable private String getContainingClassForImplicitReference() { - if (classDef.options.useImplicitReferences) { + if (classDef.options.implicitReferences) { return classDef.classDef.getType(); } return null; diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/PostInstructionRegisterInfoMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/PostInstructionRegisterInfoMethodItem.java index 812a282a..62826b1e 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/PostInstructionRegisterInfoMethodItem.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/PostInstructionRegisterInfoMethodItem.java @@ -28,7 +28,7 @@ package org.jf.baksmali.Adaptors; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.dexlib2.analysis.AnalyzedInstruction; import org.jf.dexlib2.analysis.RegisterType; import org.jf.util.IndentingWriter; @@ -60,12 +60,12 @@ public class PostInstructionRegisterInfoMethodItem extends MethodItem { int registerCount = analyzedInstruction.getRegisterCount(); BitSet registers = new BitSet(registerCount); - if ((registerInfo & baksmaliOptions.ALL) != 0) { + if ((registerInfo & BaksmaliOptions.ALL) != 0) { registers.set(0, registerCount); } else { - if ((registerInfo & baksmaliOptions.ALLPOST) != 0) { + if ((registerInfo & BaksmaliOptions.ALLPOST) != 0) { registers.set(0, registerCount); - } else if ((registerInfo & baksmaliOptions.DEST) != 0) { + } else if ((registerInfo & BaksmaliOptions.DEST) != 0) { addDestRegs(registers, registerCount); } } diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/PreInstructionRegisterInfoMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/PreInstructionRegisterInfoMethodItem.java index f5329388..f934eddb 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/PreInstructionRegisterInfoMethodItem.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/PreInstructionRegisterInfoMethodItem.java @@ -28,7 +28,7 @@ package org.jf.baksmali.Adaptors; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.dexlib2.analysis.AnalyzedInstruction; import org.jf.dexlib2.analysis.MethodAnalyzer; import org.jf.dexlib2.analysis.RegisterType; @@ -68,29 +68,29 @@ public class PreInstructionRegisterInfoMethodItem extends MethodItem { BitSet registers = new BitSet(registerCount); BitSet mergeRegisters = null; - if ((registerInfo & baksmaliOptions.ALL) != 0) { + if ((registerInfo & BaksmaliOptions.ALL) != 0) { registers.set(0, registerCount); } else { - if ((registerInfo & baksmaliOptions.ALLPRE) != 0) { + if ((registerInfo & BaksmaliOptions.ALLPRE) != 0) { registers.set(0, registerCount); } else { - if ((registerInfo & baksmaliOptions.ARGS) != 0) { + if ((registerInfo & BaksmaliOptions.ARGS) != 0) { addArgsRegs(registers); } - if ((registerInfo & baksmaliOptions.MERGE) != 0) { + if ((registerInfo & BaksmaliOptions.MERGE) != 0) { if (analyzedInstruction.isBeginningInstruction()) { addParamRegs(registers, registerCount); } mergeRegisters = new BitSet(registerCount); addMergeRegs(mergeRegisters, registerCount); - } else if ((registerInfo & baksmaliOptions.FULLMERGE) != 0 && + } else if ((registerInfo & BaksmaliOptions.FULLMERGE) != 0 && (analyzedInstruction.isBeginningInstruction())) { addParamRegs(registers, registerCount); } } } - if ((registerInfo & baksmaliOptions.FULLMERGE) != 0) { + if ((registerInfo & BaksmaliOptions.FULLMERGE) != 0) { if (mergeRegisters == null) { mergeRegisters = new BitSet(registerCount); addMergeRegs(mergeRegisters, registerCount); diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/RegisterFormatter.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/RegisterFormatter.java index bffcb385..3d72f468 100644 --- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/RegisterFormatter.java +++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/RegisterFormatter.java @@ -28,7 +28,7 @@ package org.jf.baksmali.Adaptors; -import org.jf.baksmali.baksmaliOptions; +import org.jf.baksmali.BaksmaliOptions; import org.jf.util.IndentingWriter; import javax.annotation.Nonnull; @@ -38,11 +38,11 @@ import java.io.IOException; * This class contains the logic used for formatting registers */ public class RegisterFormatter { - @Nonnull public final baksmaliOptions options; + @Nonnull public final BaksmaliOptions options; public final int registerCount; public final int parameterRegisterCount; - public RegisterFormatter(@Nonnull baksmaliOptions options, int registerCount, int parameterRegisterCount) { + public RegisterFormatter(@Nonnull BaksmaliOptions options, int registerCount, int parameterRegisterCount) { this.options = options; this.registerCount = registerCount; this.parameterRegisterCount = parameterRegisterCount; @@ -58,7 +58,7 @@ public class RegisterFormatter { * @param lastRegister the last register in the range */ public void writeRegisterRange(IndentingWriter writer, int startRegister, int lastRegister) throws IOException { - if (!options.noParameterRegisters) { + if (options.parameterRegisters) { assert startRegister <= lastRegister; if (startRegister >= registerCount - parameterRegisterCount) { @@ -86,7 +86,7 @@ public class RegisterFormatter { * @param register the register number */ public void writeTo(IndentingWriter writer, int register) throws IOException { - if (!options.noParameterRegisters) { + if (options.parameterRegisters) { if (register >= registerCount - parameterRegisterCount) { writer.write('p'); writer.printSignedIntAsDec((register - (registerCount - parameterRegisterCount))); diff --git a/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java b/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java new file mode 100644 index 00000000..20bc45be --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java @@ -0,0 +1,143 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.Parameter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import org.jf.dexlib2.analysis.ClassPath; +import org.jf.dexlib2.analysis.ClassPathResolver; +import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; +import org.jf.dexlib2.iface.DexFile; +import org.jf.util.jcommander.ColonParameterSplitter; +import org.jf.util.jcommander.ExtendedParameter; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.util.List; + +import static org.jf.dexlib2.analysis.ClassPath.NOT_ART; + +public class AnalysisArguments { + @Parameter(names = {"-b", "--bootclasspath", "--bcp"}, + description = "A colon separated list of the files to include in the bootclasspath when analyzing the " + + "dex file. If not specified, baksmali will attempt to choose an " + + "appropriate default. When analyzing oat files, this can simply be the path to the device's " + + "boot.oat file. A single empty string can be used to specify that an empty bootclasspath should " + + "be used. (e.g. --bootclasspath \"\") See baksmali help classpath for more information.", + splitter = ColonParameterSplitter.class) + @ExtendedParameter(argumentNames = "classpath") + public List<String> bootClassPath = null; + + @Parameter(names = {"-c", "--classpath", "--cp"}, + description = "A colon separated list of additional files to include in the classpath when analyzing the " + + "dex file. These will be added to the classpath after any bootclasspath entries.", + splitter = ColonParameterSplitter.class) + @ExtendedParameter(argumentNames = "classpath") + public List<String> classPath = Lists.newArrayList(); + + @Parameter(names = {"-d", "--classpath-dir", "--cpd", "--dir"}, + description = "A directory to search for classpath files. This option can be used multiple times to " + + "specify multiple directories to search. They will be searched in the order they are provided.") + @ExtendedParameter(argumentNames = "dir") + public List<String> classPathDirectories = null; + + public static class CheckPackagePrivateArgument { + @Parameter(names = {"--check-package-private-access", "--package-private", "--checkpp", "--pp"}, + description = "Use the package-private access check when calculating vtable indexes. This is enabled " + + "by default for oat files. For odex files, this is only needed for odexes from 4.2.0. It " + + "was reverted in 4.2.1.") + public boolean checkPackagePrivateAccess = false; + } + + @Nonnull + public ClassPath loadClassPathForDexFile(@Nonnull File dexFileDir, @Nonnull DexFile dexFile, + boolean checkPackagePrivateAccess) throws IOException { + return loadClassPathForDexFile(dexFileDir, dexFile, checkPackagePrivateAccess, NOT_ART); + } + + @Nonnull + public ClassPath loadClassPathForDexFile(@Nonnull File dexFileDir, @Nonnull DexFile dexFile, + boolean checkPackagePrivateAccess, int oatVersion) + throws IOException { + ClassPathResolver resolver; + + // By default, oatVersion should be NOT_ART, and we'll automatically set it if dexFile is an oat file. In some + // cases the caller may choose to override the oat version, in which case we should use the given oat version + // regardless of the actual version of the oat file + if (oatVersion == NOT_ART) { + if (dexFile instanceof OatDexFile) { + checkPackagePrivateAccess = true; + oatVersion = ((OatDexFile)dexFile).getContainer().getOatVersion(); + } + } else { + // this should always be true for ART + checkPackagePrivateAccess = true; + } + + if (classPathDirectories == null || classPathDirectories.size() == 0) { + classPathDirectories = Lists.newArrayList(dexFileDir.getPath()); + } + + List<String> filteredClassPathDirectories = Lists.newArrayList(); + if (classPathDirectories != null) { + for (String dir: classPathDirectories) { + File file = new File(dir); + if (!file.exists()) { + System.err.println(String.format("Warning: directory %s does not exist. Ignoring.", dir)); + } else if (!file.isDirectory()) { + System.err.println(String.format("Warning: %s is not a directory. Ignoring.", dir)); + } else { + filteredClassPathDirectories.add(dir); + } + } + } + + if (bootClassPath == null) { + // TODO: we should be able to get the api from the Opcodes object associated with the dexFile.. + // except that the oat version -> api mapping doesn't fully work yet + resolver = new ClassPathResolver(filteredClassPathDirectories, classPath, dexFile); + } else if (bootClassPath.size() == 1 && bootClassPath.get(0).length() == 0) { + // --bootclasspath "" is a special case, denoting that no bootclasspath should be used + resolver = new ClassPathResolver( + ImmutableList.<String>of(), ImmutableList.<String>of(), classPath, dexFile); + } else { + resolver = new ClassPathResolver(filteredClassPathDirectories, bootClassPath, classPath, dexFile); + } + + if (oatVersion == 0 && dexFile instanceof OatDexFile) { + oatVersion = ((OatDexFile)dexFile).getContainer().getOatVersion(); + } + return new ClassPath(resolver.getResolvedClassProviders(), checkPackagePrivateAccess, oatVersion); + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/baksmali.java b/baksmali/src/main/java/org/jf/baksmali/Baksmali.java index 50607340..1c0231b5 100644 --- a/baksmali/src/main/java/org/jf/baksmali/baksmali.java +++ b/baksmali/src/main/java/org/jf/baksmali/Baksmali.java @@ -28,105 +28,28 @@ package org.jf.baksmali; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import org.jf.baksmali.Adaptors.ClassDefinition; -import org.jf.dexlib2.analysis.ClassPath; -import org.jf.dexlib2.analysis.CustomInlineMethodResolver; import org.jf.dexlib2.iface.ClassDef; import org.jf.dexlib2.iface.DexFile; -import org.jf.dexlib2.util.SyntheticAccessorResolver; import org.jf.util.ClassFileNameHandler; import org.jf.util.IndentingWriter; -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; +import javax.annotation.Nullable; import java.io.*; +import java.util.HashSet; import java.util.List; -import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.*; -public class baksmali { - - public static boolean disassembleDexFile(DexFile dexFile, final baksmaliOptions options) { - if (options.registerInfo != 0 || options.deodex || options.normalizeVirtualMethods) { - try { - Iterable<String> extraClassPathEntries; - if (options.extraClassPathEntries != null) { - extraClassPathEntries = options.extraClassPathEntries; - } else { - extraClassPathEntries = ImmutableList.of(); - } - - options.classPath = ClassPath.fromClassPath(options.bootClassPathDirs, - Iterables.concat(options.bootClassPathEntries, extraClassPathEntries), dexFile, - options.apiLevel, options.checkPackagePrivateAccess, options.experimental); - - if (options.customInlineDefinitions != null) { - options.inlineResolver = new CustomInlineMethodResolver(options.classPath, - options.customInlineDefinitions); - } - } catch (Exception ex) { - System.err.println("\n\nError occurred while loading boot class path files. Aborting."); - ex.printStackTrace(System.err); - return false; - } - } - - if (options.resourceIdFileEntries != null) { - class PublicHandler extends DefaultHandler { - String prefix = null; - public PublicHandler(String prefix) { - super(); - this.prefix = prefix; - } - - public void startElement(String uri, String localName, - String qName, Attributes attr) throws SAXException { - if (qName.equals("public")) { - String type = attr.getValue("type"); - String name = attr.getValue("name").replace('.', '_'); - Integer public_key = Integer.decode(attr.getValue("id")); - String public_val = new StringBuffer() - .append(prefix) - .append(".") - .append(type) - .append(".") - .append(name) - .toString(); - options.resourceIds.put(public_key, public_val); - } - } - }; - - for (Entry<String,String> entry: options.resourceIdFileEntries.entrySet()) { - try { - SAXParser saxp = SAXParserFactory.newInstance().newSAXParser(); - String prefix = entry.getValue(); - saxp.parse(entry.getKey(), new PublicHandler(prefix)); - } catch (ParserConfigurationException e) { - continue; - } catch (SAXException e) { - continue; - } catch (IOException e) { - continue; - } - } - } +public class Baksmali { + public static boolean disassembleDexFile(DexFile dexFile, File outputDir, int jobs, final BaksmaliOptions options) { + return disassembleDexFile(dexFile, outputDir, jobs, options, null); + } - File outputDirectoryFile = new File(options.outputDirectory); - if (!outputDirectoryFile.exists()) { - if (!outputDirectoryFile.mkdirs()) { - System.err.println("Can't create the output directory " + options.outputDirectory); - return false; - } - } + public static boolean disassembleDexFile(DexFile dexFile, File outputDir, int jobs, final BaksmaliOptions options, + @Nullable List<String> classes) { //sort the classes, so that if we're on a case-insensitive file system and need to handle classes with file //name collisions, then we'll use the same name for each class, if the dex file goes through multiple @@ -134,16 +57,20 @@ public class baksmali { //may still change of course List<? extends ClassDef> classDefs = Ordering.natural().sortedCopy(dexFile.getClasses()); - if (!options.noAccessorComments) { - options.syntheticAccessorResolver = new SyntheticAccessorResolver(dexFile.getOpcodes(), classDefs); - } - - final ClassFileNameHandler fileNameHandler = new ClassFileNameHandler(outputDirectoryFile, ".smali"); + final ClassFileNameHandler fileNameHandler = new ClassFileNameHandler(outputDir, ".smali"); - ExecutorService executor = Executors.newFixedThreadPool(options.jobs); + ExecutorService executor = Executors.newFixedThreadPool(jobs); List<Future<Boolean>> tasks = Lists.newArrayList(); + Set<String> classSet = null; + if (classes != null) { + classSet = new HashSet<String>(classes); + } + for (final ClassDef classDef: classDefs) { + if (classSet != null && !classSet.contains(classDef.getType())) { + continue; + } tasks.add(executor.submit(new Callable<Boolean>() { @Override public Boolean call() throws Exception { return disassembleClass(classDef, fileNameHandler, options); @@ -174,7 +101,7 @@ public class baksmali { } private static boolean disassembleClass(ClassDef classDef, ClassFileNameHandler fileNameHandler, - baksmaliOptions options) { + BaksmaliOptions options) { /** * The path for the disassembly file is based on the package name * The class descriptor will look something like: diff --git a/baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java b/baksmali/src/main/java/org/jf/baksmali/BaksmaliOptions.java index 32685ddf..7ad51243 100644 --- a/baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java +++ b/baksmali/src/main/java/org/jf/baksmali/BaksmaliOptions.java @@ -31,19 +31,35 @@ package org.jf.baksmali; -import com.google.common.collect.Lists; import org.jf.dexlib2.analysis.ClassPath; import org.jf.dexlib2.analysis.InlineMethodResolver; import org.jf.dexlib2.util.SyntheticAccessorResolver; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; -import javax.annotation.Nullable; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; import java.io.File; -import java.util.Arrays; +import java.io.IOException; import java.util.HashMap; -import java.util.List; import java.util.Map; -public class baksmaliOptions { +public class BaksmaliOptions { + public int apiLevel = 15; + + public boolean parameterRegisters = true; + public boolean localsDirective = false; + public boolean sequentialLabels = false; + public boolean debugInfo = true; + public boolean codeOffsets = false; + public boolean accessorComments = true; + public boolean allowOdex = false; + public boolean deodex = false; + public boolean implicitReferences = false; + public boolean normalizeVirtualMethods = false; + // register info values public static final int ALL = 1; public static final int ALLPRE = 2; @@ -53,56 +69,40 @@ public class baksmaliOptions { public static final int MERGE = 32; public static final int FULLMERGE = 64; - public int apiLevel = 15; - public String outputDirectory = "out"; - @Nullable public String dexEntry = null; - public List<String> bootClassPathDirs = Lists.newArrayList(); - - public List<String> bootClassPathEntries = Lists.newArrayList(); - public List<String> extraClassPathEntries = Lists.newArrayList(); + public int registerInfo = 0; - public Map<String,String> resourceIdFileEntries = new HashMap<String,String>(); public Map<Integer,String> resourceIds = new HashMap<Integer,String>(); - - public boolean noParameterRegisters = false; - public boolean useLocalsDirective = false; - public boolean useSequentialLabels = false; - public boolean outputDebugInfo = true; - public boolean addCodeOffsets = false; - public boolean noAccessorComments = false; - public boolean allowOdex = false; - public boolean deodex = false; - public boolean experimental = false; - public boolean ignoreErrors = false; - public boolean checkPackagePrivateAccess = false; - public boolean useImplicitReferences = false; - public boolean normalizeVirtualMethods = false; - public File customInlineDefinitions = null; public InlineMethodResolver inlineResolver = null; - public int registerInfo = 0; public ClassPath classPath = null; - public int jobs = Runtime.getRuntime().availableProcessors(); - public boolean disassemble = true; - public boolean dump = false; - public String dumpFileName = null; - public SyntheticAccessorResolver syntheticAccessorResolver = null; - public void setBootClassPath(String bootClassPath) { - bootClassPathEntries = Lists.newArrayList(bootClassPath.split(":")); - } - - public void addExtraClassPath(String extraClassPath) { - if (extraClassPath.startsWith(":")) { - extraClassPath = extraClassPath.substring(1); - } - extraClassPathEntries.addAll(Arrays.asList(extraClassPath.split(":"))); - } - - public void setResourceIdFiles(String resourceIdFiles) { - for (String resourceIdFile: resourceIdFiles.split(":")) { - String[] entry = resourceIdFile.split("="); - resourceIdFileEntries.put(entry[1], entry[0]); + /** + * Load the resource ids from a set of public.xml files. + * + * @param resourceFiles A map of resource prefixes -> public.xml files + */ + public void loadResourceIds(Map<String, File> resourceFiles) throws SAXException, IOException { + for (Map.Entry<String, File> entry: resourceFiles.entrySet()) { + try { + SAXParser saxp = SAXParserFactory.newInstance().newSAXParser(); + final String prefix = entry.getKey(); + saxp.parse(entry.getValue(), new DefaultHandler() { + @Override + public void startElement(String uri, String localName, String qName, + Attributes attr) throws SAXException { + if (qName.equals("public")) { + String resourceType = attr.getValue("type"); + String resourceName = attr.getValue("name").replace('.', '_'); + Integer resourceId = Integer.decode(attr.getValue("id")); + String qualifiedResourceName = + String.format("%s.%s.%s", prefix, resourceType, resourceName); + resourceIds.put(resourceId, qualifiedResourceName); + } + } + }); + } catch (ParserConfigurationException ex) { + throw new RuntimeException(ex); + } } } } diff --git a/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java b/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java new file mode 100644 index 00000000..3ded479f --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java @@ -0,0 +1,109 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.beust.jcommander.ParametersDelegate; +import org.jf.baksmali.AnalysisArguments.CheckPackagePrivateArgument; +import org.jf.dexlib2.analysis.CustomInlineMethodResolver; +import org.jf.dexlib2.analysis.InlineMethodResolver; +import org.jf.dexlib2.dexbacked.DexBackedOdexFile; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.util.List; + +@Parameters(commandDescription = "Deodexes an odex/oat file") +@ExtendedParameters( + commandName = "deodex", + commandAliases = { "de", "x" }) +public class DeodexCommand extends DisassembleCommand { + + @ParametersDelegate + protected CheckPackagePrivateArgument checkPackagePrivateArgument = new CheckPackagePrivateArgument(); + + @Parameter(names = {"--inline-table", "--inline", "--it"}, + description = "Specify a file containing a custom inline method table to use. See the " + + "\"deodexerant\" tool in the smali github repository to dump the inline method table from a " + + "device that uses dalvik.") + @ExtendedParameter(argumentNames = "file") + private String inlineTable; + + public DeodexCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + @Override protected BaksmaliOptions getOptions() { + BaksmaliOptions options = super.getOptions(); + + options.deodex = true; + + if (dexFile instanceof DexBackedOdexFile) { + if (inlineTable == null) { + options.inlineResolver = InlineMethodResolver.createInlineMethodResolver( + ((DexBackedOdexFile)dexFile).getOdexVersion()); + } else { + File inlineTableFile = new File(inlineTable); + if (!inlineTableFile.exists()) { + System.err.println(String.format("Could not find file: %s", inlineTable)); + System.exit(-1); + } + try { + options.inlineResolver = new CustomInlineMethodResolver(options.classPath, inlineTableFile); + } catch (IOException ex) { + System.err.println(String.format("Error while reading file: %s", inlineTableFile)); + ex.printStackTrace(System.err); + System.exit(-1); + } + } + } + + return options; + } + + @Override protected boolean shouldCheckPackagePrivateAccess() { + return checkPackagePrivateArgument.checkPackagePrivateAccess; + } + + @Override protected boolean needsClassPath() { + return true; + } + + @Override protected boolean showDeodexWarning() { + return false; + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java b/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java new file mode 100644 index 00000000..c7660cbe --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java @@ -0,0 +1,150 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import org.jf.dexlib2.DexFileFactory; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedParameter; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * This class implements common functionality for commands that need to load a dex file based on + * command line input + */ +public abstract class DexInputCommand extends Command { + + @Parameter(names = {"-a", "--api"}, + description = "The numeric api level of the file being disassembled.") + @ExtendedParameter(argumentNames = "api") + public int apiLevel = 15; + + @Parameter(description = "A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " + + "files, you can specify the specific entry to use as if the apk/oat file was a directory. " + + "e.g. \"app.apk/classes2.dex\". For more information, see \"baksmali help input\".") + @ExtendedParameter(argumentNames = "file") + protected List<String> inputList = Lists.newArrayList(); + + protected File inputFile; + protected String inputEntry; + protected DexBackedDexFile dexFile; + + public DexInputCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + /** + * Parses a dex file input from the user and loads the given dex file. + * + * In some cases, the input file can contain multiple dex files. If this is the case, you can refer to a specific + * dex file with a slash, followed by the entry name, optionally in quotes. + * + * If the entry name is enclosed in quotes, then it will strip the first and last quote and look for an entry with + * exactly that name. Otherwise, it will perform a partial filename match against the entry to find any candidates. + * If there is a single matching candidate, it will be used. Otherwise, an error will be generated. + * + * For example, to refer to the "/system/framework/framework.jar:classes2.dex" entry within the + * "framework/arm/framework.oat" oat file, you could use any of: + * + * framework/arm/framework.oat/"/system/framework/framework.jar:classes2.dex" + * framework/arm/framework.oat/system/framework/framework.jar:classes2.dex + * framework/arm/framework.oat/framework/framework.jar:classes2.dex + * framework/arm/framework.oat/framework.jar:classes2.dex + * framework/arm/framework.oat/classes2.dex + * + * The last option is the easiest, but only works if the oat file doesn't contain another entry with the + * "classes2.dex" name. e.g. "/system/framework/blah.jar:classes2.dex" + * + * It's technically possible (although unlikely) for an oat file to contain 2 entries like: + * /system/framework/framework.jar:classes2.dex + * system/framework/framework.jar:classes2.dex + * + * In this case, the "framework/arm/framework.oat/system/framework/framework.jar:classes2.dex" syntax will generate + * an error because both entries match the partial entry name. Instead, you could use the following for the + * first and second entry respectively: + * + * framework/arm/framework.oat/"/system/framework/framework.jar:classes2.dex" + * framework/arm/framework.oat/"system/framework/framework.jar:classes2.dex" + * + * @param input The name of a dex, apk, odex or oat file/entry. + */ + protected void loadDexFile(@Nonnull String input) { + File file = new File(input); + + while (file != null && !file.exists()) { + file = file.getParentFile(); + } + + if (file == null || !file.exists() || file.isDirectory()) { + System.err.println("Can't find file: " + input); + System.exit(1); + } + + inputFile = file; + + String dexEntry = null; + if (file.getPath().length() < input.length()) { + dexEntry = input.substring(file.getPath().length() + 1); + } + + if (!Strings.isNullOrEmpty(dexEntry)) { + boolean exactMatch = false; + if (dexEntry.length() > 2 && dexEntry.charAt(0) == '"' && dexEntry.charAt(dexEntry.length() - 1) == '"') { + dexEntry = dexEntry.substring(1, dexEntry.length() - 1); + exactMatch = true; + } + + inputEntry = dexEntry; + + try { + dexFile = DexFileFactory.loadDexEntry(file, dexEntry, exactMatch, Opcodes.forApi(apiLevel)); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } else { + try { + dexFile = DexFileFactory.loadDexFile(file, Opcodes.forApi(apiLevel)); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java b/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java new file mode 100644 index 00000000..2e3eb79e --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java @@ -0,0 +1,287 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.beust.jcommander.ParametersDelegate; +import com.beust.jcommander.validators.PositiveInteger; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.jf.dexlib2.util.SyntheticAccessorResolver; +import org.jf.util.StringWrapper; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; +import org.xml.sax.SAXException; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@Parameters(commandDescription = "Disassembles a dex file.") +@ExtendedParameters( + commandName = "disassemble", + commandAliases = { "dis", "d" }) +public class DisassembleCommand extends DexInputCommand { + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information for this command.") + private boolean help; + + @ParametersDelegate + protected AnalysisArguments analysisArguments = new AnalysisArguments(); + + @Parameter(names = {"--debug-info", "--di"}, arity = 1, + description = "Whether to include debug information in the output (.local, .param, .line, etc.). True " + + "by default, use --debug-info=false to disable.") + @ExtendedParameter(argumentNames = "boolean") + private boolean debugInfo = true; + + @Parameter(names = {"--code-offsets", "--offsets", "--off"}, + description = "Add a comment before each instruction with it's code offset within the method.") + private boolean codeOffsets = false; + + @Parameter(names = {"--resolve-resources", "--rr"}, arity = 2, + description = "This will attempt to find any resource id references within the bytecode and add a " + + "comment with the name of the resource being referenced. The parameter accepts 2 values:" + + "an arbitrary resource prefix and the path to a public.xml file. For example: " + + "--resolve-resources android.R framework/res/values/public.xml. This option can be specified " + + "multiple times to provide resources from multiple packages.") + @ExtendedParameter(argumentNames = {"resource prefix", "public.xml file"}) + private List<String> resourceIdFiles = Lists.newArrayList(); + + @Parameter(names = {"-j", "--jobs"}, + description = "The number of threads to use. Defaults to the number of cores available.", + validateWith = PositiveInteger.class) + @ExtendedParameter(argumentNames = "n") + private int jobs = Runtime.getRuntime().availableProcessors(); + + @Parameter(names = {"-l", "--use-locals"}, + description = "When disassembling, output the .locals directive with the number of non-parameter " + + "registers instead of the .registers directive with the total number of registers.") + private boolean localsDirective = false; + + @Parameter(names = {"--accessor-comments", "--ac"}, arity = 1, + description = "Generate helper comments for synthetic accessors. True by default, use " + + "--accessor-comments=false to disable.") + @ExtendedParameter(argumentNames = "boolean") + private boolean accessorComments = true; + + @Parameter(names = {"--normalize-virtual-methods", "--norm", "--nvm"}, + description = "Normalize virtual method references to use the base class where the method is " + + "originally declared.") + private boolean normalizeVirtualMethods = false; + + @Parameter(names = {"-o", "--output"}, + description = "The directory to write the disassembled files to.") + @ExtendedParameter(argumentNames = "dir") + private String outputDir = "out"; + + @Parameter(names = {"--parameter-registers", "--preg", "--pr"}, arity = 1, + description = "Use the pNN syntax for registers that refer to a method parameter on method entry. True " + + "by default, use --parameter-registers=false to disable.") + @ExtendedParameter(argumentNames = "boolean") + private boolean parameterRegisters = true; + + @Parameter(names = {"-r", "--register-info"}, + description = "Add comments before/after each instruction with information about register types. " + + "The value is a comma-separated list of any of ALL, ALLPRE, ALLPOST, ARGS, DEST, MERGE and " + + "FULLMERGE. See \"baksmali help register-info\" for more information.") + @ExtendedParameter(argumentNames = "register info specifier") + private List<String> registerInfoTypes = Lists.newArrayList(); + + @Parameter(names = {"--sequential-labels", "--seq", "--sl"}, + description = "Create label names using a sequential numbering scheme per label type, rather than " + + "using the bytecode address.") + private boolean sequentialLabels = false; + + @Parameter(names = {"--implicit-references", "--implicit", "--ir"}, + description = "Use implicit method and field references (without the class name) for methods and " + + "fields from the current class.") + private boolean implicitReferences = false; + + @Parameter(names = "--classes", + description = "A comma separated list of classes. Only disassemble these classes") + @ExtendedParameter(argumentNames = "classes") + private List<String> classes = null; + + public DisassembleCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + public void run() { + if (help || inputList == null || inputList.isEmpty()) { + usage(); + return; + } + + if (inputList.size() > 1) { + System.err.println("Too many files specified"); + usage(); + return; + } + + String input = inputList.get(0); + loadDexFile(input); + + if (showDeodexWarning() && dexFile.hasOdexOpcodes()) { + StringWrapper.printWrappedString(System.err, + "Warning: You are disassembling an odex/oat file without deodexing it. You won't be able to " + + "re-assemble the results unless you deodex it. See \"baksmali help deodex\""); + } + + File outputDirectoryFile = new File(outputDir); + if (!outputDirectoryFile.exists()) { + if (!outputDirectoryFile.mkdirs()) { + System.err.println("Can't create the output directory " + outputDir); + System.exit(-1); + } + } + + if (analysisArguments.classPathDirectories == null || analysisArguments.classPathDirectories.isEmpty()) { + analysisArguments.classPathDirectories = Lists.newArrayList(inputFile.getAbsoluteFile().getParent()); + } + + if (!Baksmali.disassembleDexFile(dexFile, outputDirectoryFile, jobs, getOptions(), classes)) { + System.exit(-1); + } + } + + protected boolean needsClassPath() { + return !registerInfoTypes.isEmpty() || normalizeVirtualMethods; + } + + protected boolean shouldCheckPackagePrivateAccess() { + return false; + } + + protected boolean showDeodexWarning() { + return true; + } + + protected BaksmaliOptions getOptions() { + if (dexFile == null) { + throw new IllegalStateException("You must call loadDexFile first"); + } + + final BaksmaliOptions options = new BaksmaliOptions(); + + if (needsClassPath()) { + try { + options.classPath = analysisArguments.loadClassPathForDexFile( + inputFile.getAbsoluteFile().getParentFile(), dexFile, shouldCheckPackagePrivateAccess()); + } catch (Exception ex) { + System.err.println("\n\nError occurred while loading class path files. Aborting."); + ex.printStackTrace(System.err); + System.exit(-1); + } + } + + if (!resourceIdFiles.isEmpty()) { + Map<String, File> resourceFiles = Maps.newHashMap(); + + assert (resourceIdFiles.size() % 2) == 0; + for (int i=0; i<resourceIdFiles.size(); i+=2) { + String resourcePrefix = resourceIdFiles.get(i); + String publicXml = resourceIdFiles.get(i+1); + + File publicXmlFile = new File(publicXml); + + if (!publicXmlFile.exists()) { + System.err.println(String.format("Can't find file: %s", publicXmlFile)); + System.exit(-1); + } + + resourceFiles.put(resourcePrefix, publicXmlFile); + } + + try { + options.loadResourceIds(resourceFiles); + } catch (IOException ex) { + System.err.println("Error while loading resource files:"); + ex.printStackTrace(System.err); + System.exit(-1); + } catch (SAXException ex) { + System.err.println("Error while loading resource files:"); + ex.printStackTrace(System.err); + System.exit(-1); + } + } + + options.parameterRegisters = parameterRegisters; + options.localsDirective = localsDirective; + options.sequentialLabels = sequentialLabels; + options.debugInfo = debugInfo; + options.codeOffsets = codeOffsets; + options.accessorComments = accessorComments; + options.implicitReferences = implicitReferences; + options.normalizeVirtualMethods = normalizeVirtualMethods; + + options.registerInfo = 0; + + for (String registerInfoType: registerInfoTypes) { + if (registerInfoType.equalsIgnoreCase("ALL")) { + options.registerInfo |= BaksmaliOptions.ALL; + } else if (registerInfoType.equalsIgnoreCase("ALLPRE")) { + options.registerInfo |= BaksmaliOptions.ALLPRE; + } else if (registerInfoType.equalsIgnoreCase("ALLPOST")) { + options.registerInfo |= BaksmaliOptions.ALLPOST; + } else if (registerInfoType.equalsIgnoreCase("ARGS")) { + options.registerInfo |= BaksmaliOptions.ARGS; + } else if (registerInfoType.equalsIgnoreCase("DEST")) { + options.registerInfo |= BaksmaliOptions.DEST; + } else if (registerInfoType.equalsIgnoreCase("MERGE")) { + options.registerInfo |= BaksmaliOptions.MERGE; + } else if (registerInfoType.equalsIgnoreCase("FULLMERGE")) { + options.registerInfo |= BaksmaliOptions.FULLMERGE; + } else { + System.err.println(String.format("Invalid register info type: %s", registerInfoType)); + usage(); + System.exit(-1); + } + + if ((options.registerInfo & BaksmaliOptions.FULLMERGE) != 0) { + options.registerInfo &= ~BaksmaliOptions.MERGE; + } + } + + if (accessorComments) { + options.syntheticAccessorResolver = new SyntheticAccessorResolver(dexFile.getOpcodes(), + dexFile.getClasses()); + } + + return options; + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java b/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java new file mode 100644 index 00000000..433be125 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.dexlib2.dexbacked.raw.RawDexFile; +import org.jf.dexlib2.dexbacked.raw.util.DexAnnotator; +import org.jf.util.ConsoleUtil; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.io.*; +import java.util.List; + +@Parameters(commandDescription = "Prints an annotated hex dump for the given dex file") +@ExtendedParameters( + commandName = "dump", + commandAliases = "du") +public class DumpCommand extends DexInputCommand { + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information for this command.") + private boolean help; + + public DumpCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + public void run() { + if (help || inputList == null || inputList.isEmpty()) { + usage(); + return; + } + + if (inputList.size() > 1) { + System.err.println("Too many files specified"); + usage(); + return; + } + + String input = inputList.get(0); + loadDexFile(input); + + try { + dump(dexFile, System.out); + } catch (IOException ex) { + System.err.println("There was an error while dumping the dex file"); + ex.printStackTrace(System.err); + } + } + + /** + * Writes an annotated hex dump of the given dex file to output. + * + * @param dexFile The dex file to dump + * @param output An OutputStream to write the annotated hex dump to. The caller is responsible for closing this + * when needed. + * + * @throws IOException + */ + public static void dump(@Nonnull DexBackedDexFile dexFile, @Nonnull OutputStream output) + throws IOException { + Writer writer = new BufferedWriter(new OutputStreamWriter(output)); + + int consoleWidth = ConsoleUtil.getConsoleWidth(); + if (consoleWidth <= 0) { + consoleWidth = 120; + } + + RawDexFile rawDexFile = new RawDexFile(dexFile.getOpcodes(), dexFile); + DexAnnotator annotator = new DexAnnotator(rawDexFile, consoleWidth); + annotator.writeAnnotations(writer); + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java b/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java new file mode 100644 index 00000000..149ac63d --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java @@ -0,0 +1,204 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.collect.Lists; +import org.jf.util.ConsoleUtil; +import org.jf.util.StringWrapper; +import org.jf.util.jcommander.*; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Shows usage information") +@ExtendedParameters( + commandName = "help", + commandAliases = "h") +public class HelpCommand extends Command { + + public HelpCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + @Parameter(description = "If specified, show the detailed usage information for the given commands") + @ExtendedParameter(argumentNames = "commands") + private List<String> commands = Lists.newArrayList(); + + public void run() { + JCommander parentJc = commandAncestors.get(commandAncestors.size() - 1); + + if (commands == null || commands.isEmpty()) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); + } else { + boolean printedHelp = false; + for (String cmd : commands) { + if (cmd.equals("register-info")) { + printedHelp = true; + String registerInfoHelp = "The --register-info parameter will cause baksmali to generate " + + "comments before and after every instruction containing register type " + + "information about some subset of registers. This parameter accepts a comma-separated list" + + "of values specifying which registers and how much information to include.\n" + + " ALL: all pre- and post-instruction registers\n" + + " ALLPRE: all pre-instruction registers\n" + + " ALLPOST: all post-instruction registers\n" + + " ARGS: any pre-instruction registers used as arguments to the instruction\n" + + " DEST: the post-instruction register used as the output of the instruction\n" + + " MERGE: any pre-instruction register that has been merged from multiple " + + "incoming code paths\n" + + " FULLMERGE: an extended version of MERGE that also includes a list of all " + + "the register types from incoming code paths that were merged"; + + Iterable<String> lines = StringWrapper.wrapStringOnBreaks(registerInfoHelp, + ConsoleUtil.getConsoleWidth()); + for (String line : lines) { + System.out.println(line); + } + } else if (cmd.equals("input")) { + printedHelp = true; + String registerInfoHelp = "Apks and oat files can contain multiple dex files. In order to " + + "specify a particular dex file, the basic syntax is to treat the apk/oat file as a " + + "directory. For example, to load the \"classes2.dex\" entry from \"app.apk\", you can " + + "use \"app.apk/classes2.dex\".\n" + + "\n" + + "For ease of use, you can also specify a partial path to the dex file to load. For " + + "example, to load a entry named \"/system/framework/framework.jar:classes2.dex\" from " + + "\"framework.oat\", you can use any of the following:\n" + + "\"framework.oat/classes2.dex\"\n" + + "\"framework.oat/framework.jar:classes2.dex\"\n" + + "\"framework.oat/framework/framework.jar:classes2.dex\"\n" + + "\"framework.oat/system/framework/framework.jar:classes2.dex\"\n" + + "\n" + + "In some rare cases, an oat file could have entries that can't be differentiated with " + + "the above syntax. For example \"/blah/blah.dex\" and \"blah/blah.dex\". In this case, " + + "the \"blah.oat/blah/blah.dex\" would match both entries and generate an error. To get " + + "around this, you can add double quotes around the entry name to specify an exact entry " + + "name. E.g. blah.oat/\"/blah/blah.dex\" or blah.oat/\"blah/blah.dex\" respectively."; + + Iterable<String> lines = StringWrapper.wrapStringOnBreaks(registerInfoHelp, + ConsoleUtil.getConsoleWidth()); + for (String line : lines) { + System.out.println(line); + } + } else if (cmd.equals("classpath")) { + printedHelp = true; + String registerInfoHelp = "When deodexing odex/oat files or when using the --register-info " + + "option, baksmali needs to load all classes from the framework files on the device " + + "in order to fully understand the class hierarchy. There are several options that " + + "control how baksmali finds and loads the classpath entries.\n" + + "\n"+ + "L+ devices (ART):\n" + + "When deodexing or disassembling a file from an L+ device using ART, you generally " + + "just need to specify the path to the boot.oat file via the --bootclasspath/-b " + + "parameter. On pre-N devices, the boot.oat file is self-contained and no other files are " + + "needed. In N, boot.oat was split into multiple files. In this case, the other " + + "files should be in the same directory as the boot.oat file, but you still only need to " + + "specify the boot.oat file in the --bootclasspath/-b option. The other files will be " + + "automatically loaded from the same directory.\n" + + "\n" + + "Pre-L devices (dalvik):\n" + + "When deodexing odex files from a pre-L device using dalvik, you " + + "generally just need to specify the path to a directory containing the framework files " + + "from the device via the --classpath-dir/-d option. odex files contain a list of " + + "framework files they depend on and baksmali will search for these dependencies in the " + + "directory that you specify.\n" + + "\n" + + "Dex files don't contain a list of dependencies like odex files, so when disassembling a " + + "dex file using the --register-info option, and using the framework files from a " + + "pre-L device, baksmali will attempt to use a reasonable default list of classpath files " + + "based on the api level set via the -a option. If this default list is incorrect, you " + + "can override the classpath using the --bootclasspath/-b option. This option accepts a " + + "colon separated list of classpath entries. Each entry can be specified in a few " + + "different ways.\n" + + " - A simple filename like \"framework.jar\"\n" + + " - A device path like \"/system/framework/framework.jar\"\n" + + " - A local relative or absolute path like \"/tmp/framework/framework.jar\"\n" + + "When using the first or second formats, you should also specify the directory " + + "containing the framework files via the --classpath-dir/-d option. When using the third " + + "format, this option is not needed.\n" + + "It's worth noting that the second format matches the format used by Android for the " + + "BOOTCLASSPATH environment variable, so you can simply grab the value of that variable " + + "from the device and use it as-is.\n" + + "\n" + + "Examples:\n" + + " For an M device:\n" + + " adb pull /system/framework/arm/boot.oat /tmp/boot.oat\n" + + " baksmali deodex blah.oat -b /tmp/boot.oat\n" + + " For an N+ device:\n" + + " adb pull /system/framework/arm /tmp/framework\n" + + " baksmali deodex blah.oat -b /tmp/framework/boot.oat\n" + + " For a pre-L device:\n" + + " adb pull /system/framework /tmp/framework\n" + + " baksmali deodex blah.odex -d /tmp/framework\n" + + " Using the BOOTCLASSPATH on a pre-L device:\n" + + " adb pull /system/framework /tmp/framework\n" + + " export BOOTCLASSPATH=`adb shell \"echo \\\\$BOOTCLASPATH\"`\n" + + " baksmali disassemble --register-info ARGS,DEST blah.apk -b $BOOTCLASSPATH -d " + + "/tmp/framework"; + + Iterable<String> lines = StringWrapper.wrapStringOnBreaks(registerInfoHelp, + ConsoleUtil.getConsoleWidth()); + for (String line : lines) { + System.out.println(line); + } + } else { + JCommander command = ExtendedCommands.getSubcommand(parentJc, cmd); + if (command == null) { + System.err.println("No such command: " + cmd); + } else { + printedHelp = true; + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(((Command)command.getObjects().get(0)).getCommandHierarchy())); + } + } + } + if (!printedHelp) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); + } + } + } + + @Parameters(hidden = true) + @ExtendedParameters(commandName = "hlep") + public static class HlepCommand extends HelpCommand { + public HlepCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java new file mode 100644 index 00000000..fb172bdd --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java @@ -0,0 +1,76 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import org.jf.dexlib2.iface.ClassDef; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Lists the classes in a dex file.") +@ExtendedParameters( + commandName = "classes", + commandAliases = { "class", "c" }) +public class ListClassesCommand extends DexInputCommand { + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information") + private boolean help; + + public ListClassesCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + @Override public void run() { + if (help || inputList == null || inputList.isEmpty()) { + usage(); + return; + } + + if (inputList.size() > 1) { + System.err.println("Too many files specified"); + usage(); + return; + } + + String input = inputList.get(0); + loadDexFile(input); + + for (ClassDef classDef: dexFile.getClasses()) { + System.out.println(classDef.getType()); + } + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListCommand.java new file mode 100644 index 00000000..95476208 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListCommand.java @@ -0,0 +1,85 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import org.jf.baksmali.ListHelpCommand.ListHlepCommand; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedCommands; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Lists various objects in a dex file.") +@ExtendedParameters( + commandName = "list", + commandAliases = "l") +public class ListCommand extends Command { + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information") + private boolean help; + + public ListCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + @Override protected void setupCommand(JCommander jc) { + List<JCommander> hierarchy = getCommandHierarchy(); + + ExtendedCommands.addExtendedCommand(jc, new ListStringsCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListMethodsCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListFieldsCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListTypesCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListClassesCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListDexCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListVtablesCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListFieldOffsetsCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListDependenciesCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListHelpCommand(hierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListHlepCommand(hierarchy)); + } + + @Override public void run() { + JCommander jc = getJCommander(); + if (help || jc.getParsedCommand() == null) { + usage(); + return; + } + + Command command = (Command)jc.getCommands().get(jc.getParsedCommand()).getObjects().get(0); + command.run(); + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListDependenciesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListDependenciesCommand.java new file mode 100644 index 00000000..636a87c5 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListDependenciesCommand.java @@ -0,0 +1,118 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.collect.Lists; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.dexlib2.dexbacked.DexBackedOdexFile; +import org.jf.dexlib2.dexbacked.OatFile; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.io.*; +import java.util.List; + +@Parameters(commandDescription = "Lists the stored dependencies in an odex/oat file.") +@ExtendedParameters( + commandName = "dependencies", + commandAliases = { "deps", "dep" }) +public class ListDependenciesCommand extends Command { + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information") + private boolean help; + + @Parameter(description = "An oat/odex file") + @ExtendedParameter(argumentNames = "file") + private List<String> inputList = Lists.newArrayList(); + + public ListDependenciesCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + @Override public void run() { + if (help || inputList == null || inputList.isEmpty()) { + usage(); + return; + } + + if (inputList.size() > 1) { + System.err.println("Too many files specified"); + usage(); + return; + } + + String input = inputList.get(0); + InputStream inputStream = null; + try { + inputStream = new BufferedInputStream(new FileInputStream(input)); + } catch (FileNotFoundException ex) { + System.err.println("Could not find file: " + input); + System.exit(-1); + } + + try { + OatFile oatFile = OatFile.fromInputStream(inputStream); + for (String entry: oatFile.getBootClassPath()) { + System.out.println(entry); + } + return; + } catch (OatFile.NotAnOatFileException ex) { + // ignore + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + try { + DexBackedOdexFile odexFile = DexBackedOdexFile.fromInputStream(Opcodes.getDefault(), inputStream); + for (String entry: odexFile.getDependencies()) { + System.out.println(entry); + } + return; + } catch (IOException ex) { + throw new RuntimeException(ex); + } catch (DexBackedOdexFile.NotAnOdexFile ex) { + // handled below + } catch (DexBackedDexFile.NotADexFile ex) { + // handled below + } + + System.err.println(input + " is not an odex or oat file."); + System.exit(-1); + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java new file mode 100644 index 00000000..d5862eb1 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java @@ -0,0 +1,102 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.collect.Lists; +import org.jf.dexlib2.DexFileFactory; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.dexlib2.iface.MultiDexContainer; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.IOException; +import java.util.List; + +@Parameters(commandDescription = "Lists the dex files in an apk/oat file.") +@ExtendedParameters( + commandName = "dex", + commandAliases = "d") +public class ListDexCommand extends Command { + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information") + private boolean help; + + @Parameter(description = "An apk or oat file.") + @ExtendedParameter(argumentNames = "file") + private List<String> inputList = Lists.newArrayList(); + + public ListDexCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + @Override public void run() { + if (help || inputList == null || inputList.isEmpty()) { + usage(); + return; + } + + if (inputList.size() > 1) { + System.err.println("Too many files specified"); + usage(); + return; + } + + String input = inputList.get(0); + File file = new File(input); + + if (!file.exists()) { + System.err.println(String.format("Could not find the file: %s", input)); + System.exit(-1); + } + + List<String> entries; + try { + MultiDexContainer<? extends DexBackedDexFile> container = + DexFileFactory.loadDexContainer(file, Opcodes.getDefault()); + entries = container.getDexEntryNames(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + for (String entry: entries) { + System.out.println(entry); + } + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java new file mode 100644 index 00000000..d6dd6e2b --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java @@ -0,0 +1,120 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.beust.jcommander.ParametersDelegate; +import org.jf.dexlib2.analysis.ClassProto; +import org.jf.dexlib2.iface.ClassDef; +import org.jf.dexlib2.iface.reference.FieldReference; +import org.jf.util.SparseArray; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.List; + +@Parameters(commandDescription = "Lists the instance field offsets for classes in a dex file.") +@ExtendedParameters( + commandName = "fieldoffsets", + commandAliases = { "fieldoffset", "fo" }) +public class ListFieldOffsetsCommand extends DexInputCommand { + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information") + private boolean help; + + @ParametersDelegate + private AnalysisArguments analysisArguments = new AnalysisArguments(); + + public ListFieldOffsetsCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + @Override public void run() { + if (help || inputList == null || inputList.isEmpty()) { + usage(); + return; + } + + if (inputList.size() > 1) { + System.err.println("Too many files specified"); + usage(); + return; + } + + String input = inputList.get(0); + loadDexFile(input); + BaksmaliOptions options = getOptions(); + + try { + for (ClassDef classDef: dexFile.getClasses()) { + ClassProto classProto = (ClassProto) options.classPath.getClass(classDef); + SparseArray<FieldReference> fields = classProto.getInstanceFields(); + String className = "Class " + classDef.getType() + " : " + fields.size() + " instance fields\n"; + System.out.write(className.getBytes()); + for (int i=0;i<fields.size();i++) { + String field = fields.keyAt(i) + ":" + fields.valueAt(i).getType() + " " + fields.valueAt(i).getName() + "\n"; + System.out.write(field.getBytes()); + } + System.out.write("\n".getBytes()); + } + System.out.close(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + @Nonnull + private BaksmaliOptions getOptions() { + if (dexFile == null) { + throw new IllegalStateException("You must call loadDexFile first"); + } + + final BaksmaliOptions options = new BaksmaliOptions(); + + options.apiLevel = apiLevel; + + try { + options.classPath = analysisArguments.loadClassPathForDexFile( + inputFile.getAbsoluteFile().getParentFile(), dexFile, false); + } catch (Exception ex) { + System.err.println("Error occurred while loading class path files."); + ex.printStackTrace(System.err); + System.exit(-1); + } + + return options; + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java new file mode 100644 index 00000000..c4d090dd --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameters; +import org.jf.dexlib2.ReferenceType; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Lists the fields in a dex file's field table.") +@ExtendedParameters( + commandName = "fields", + commandAliases = { "field", "f" }) +public class ListFieldsCommand extends ListReferencesCommand { + public ListFieldsCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors, ReferenceType.FIELD); + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListHelpCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListHelpCommand.java new file mode 100644 index 00000000..2e642861 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListHelpCommand.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.collect.Iterables; +import org.jf.util.ConsoleUtil; +import org.jf.util.jcommander.*; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Shows usage information") +@ExtendedParameters( + commandName = "help", + commandAliases = "h") +public class ListHelpCommand extends Command { + + @Parameter(description = "If specified, show the detailed usage information for the given commands") + @ExtendedParameter(argumentNames = "commands") + private List<String> commands; + + public ListHelpCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + public void run() { + if (commands == null || commands.isEmpty()) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); + } else { + boolean printedHelp = false; + JCommander parentJc = Iterables.getLast(commandAncestors); + for (String cmd : commands) { + JCommander command = ExtendedCommands.getSubcommand(parentJc, cmd); + if (command == null) { + System.err.println("No such command: " + cmd); + } else { + printedHelp = true; + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(((Command)command.getObjects().get(0)).getCommandHierarchy())); + } + } + if (!printedHelp) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); + } + } + } + + @Parameters(hidden = true) + @ExtendedParameters(commandName = "hlep") + public static class ListHlepCommand extends ListHelpCommand { + public ListHlepCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java new file mode 100644 index 00000000..603e7647 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameters; +import org.jf.dexlib2.ReferenceType; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Lists the methods in a dex file's method table.") +@ExtendedParameters( + commandName = "methods", + commandAliases = { "method", "m" }) +public class ListMethodsCommand extends ListReferencesCommand { + public ListMethodsCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors, ReferenceType.METHOD); + } +}
\ No newline at end of file diff --git a/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java new file mode 100644 index 00000000..da9c3e31 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import org.jf.dexlib2.iface.reference.Reference; +import org.jf.dexlib2.util.ReferenceUtil; + +import javax.annotation.Nonnull; +import java.util.List; + +public abstract class ListReferencesCommand extends DexInputCommand { + + private final int referenceType; + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information") + private boolean help; + + public ListReferencesCommand(@Nonnull List<JCommander> commandAncestors, int referenceType) { + super(commandAncestors); + this.referenceType = referenceType; + } + + @Override public void run() { + if (help || inputList == null || inputList.isEmpty()) { + usage(); + return; + } + + if (inputList.size() > 1) { + System.err.println("Too many files specified"); + usage(); + return; + } + + String input = inputList.get(0); + loadDexFile(input); + + for (Reference reference: dexFile.getReferences(referenceType)) { + System.out.println(ReferenceUtil.getReferenceString(reference)); + } + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java new file mode 100644 index 00000000..8694f911 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameters; +import org.jf.dexlib2.ReferenceType; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Lists the strings in a dex file's string table.") +@ExtendedParameters( + commandName = "strings", + commandAliases = { "string", "str", "s" }) +public class ListStringsCommand extends ListReferencesCommand { + public ListStringsCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors, ReferenceType.STRING); + } +}
\ No newline at end of file diff --git a/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java new file mode 100644 index 00000000..fbff2f29 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java @@ -0,0 +1,50 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameters; +import org.jf.dexlib2.ReferenceType; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Lists the type ids in a dex file's type table.") +@ExtendedParameters( + commandName = "types", + commandAliases = { "type", "t" }) +public class ListTypesCommand extends ListReferencesCommand { + public ListTypesCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors, ReferenceType.TYPE); + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java new file mode 100644 index 00000000..74435b3d --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java @@ -0,0 +1,157 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.beust.jcommander.ParametersDelegate; +import org.jf.baksmali.AnalysisArguments.CheckPackagePrivateArgument; +import org.jf.dexlib2.AccessFlags; +import org.jf.dexlib2.analysis.ClassProto; +import org.jf.dexlib2.iface.ClassDef; +import org.jf.dexlib2.iface.Method; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.List; + +@Parameters(commandDescription = "Lists the virtual method tables for classes in a dex file.") +@ExtendedParameters( + commandName = "vtables", + commandAliases = { "vtable", "v" }) +public class ListVtablesCommand extends DexInputCommand { + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information") + private boolean help; + + @ParametersDelegate + private AnalysisArguments analysisArguments = new AnalysisArguments(); + + @ParametersDelegate + private CheckPackagePrivateArgument checkPackagePrivateArgument = new CheckPackagePrivateArgument(); + + @Parameter(names = "--classes", + description = "A comma separated list of classes. Only print the vtable for these classes") + @ExtendedParameter(argumentNames = "classes") + private List<String> classes = null; + + @Parameter(names = "--override-oat-version", + description = "Uses a classpath for the given oat version, regardless of the actual oat version. This " + + "can be used, e.g. to list vtables from a dex file, as if they were in an oat file of the given " + + "version.") + private int oatVersion = 0; + + public ListVtablesCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + @Override public void run() { + if (help || inputList == null || inputList.isEmpty()) { + usage(); + return; + } + + if (inputList.size() > 1) { + System.err.println("Too many files specified"); + usage(); + return; + } + + String input = inputList.get(0); + loadDexFile(input); + + BaksmaliOptions options = getOptions(); + if (options == null) { + return; + } + + try { + if (classes != null && !classes.isEmpty()) { + for (String cls: classes) { + listClassVtable((ClassProto)options.classPath.getClass(cls)); + } + return; + } + + for (ClassDef classDef : dexFile.getClasses()) { + if (!AccessFlags.INTERFACE.isSet(classDef.getAccessFlags())) { + listClassVtable((ClassProto)options.classPath.getClass(classDef)); + } + } + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void listClassVtable(ClassProto classProto) throws IOException { + List<Method> methods = classProto.getVtable(); + String className = "Class " + classProto.getType() + " extends " + classProto.getSuperclass() + + " : " + methods.size() + " methods\n"; + System.out.write(className.getBytes()); + for (int i = 0; i < methods.size(); i++) { + Method method = methods.get(i); + + String methodString = i + ":" + method.getDefiningClass() + "->" + method.getName() + "("; + for (CharSequence parameter : method.getParameterTypes()) { + methodString += parameter; + } + methodString += ")" + method.getReturnType() + "\n"; + System.out.write(methodString.getBytes()); + } + System.out.write("\n".getBytes()); + } + + protected BaksmaliOptions getOptions() { + if (dexFile == null) { + throw new IllegalStateException("You must call loadDexFile first"); + } + + final BaksmaliOptions options = new BaksmaliOptions(); + + options.apiLevel = apiLevel; + + try { + options.classPath = analysisArguments.loadClassPathForDexFile(inputFile.getAbsoluteFile().getParentFile(), + dexFile, checkPackagePrivateArgument.checkPackagePrivateAccess, oatVersion); + } catch (Exception ex) { + System.err.println("Error occurred while loading class path files."); + ex.printStackTrace(System.err); + return null; + } + + return options; + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/Main.java b/baksmali/src/main/java/org/jf/baksmali/Main.java new file mode 100644 index 00000000..66d9b4f8 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/Main.java @@ -0,0 +1,126 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.google.common.collect.Lists; +import org.jf.baksmali.HelpCommand.HlepCommand; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedCommands; +import org.jf.util.jcommander.ExtendedParameters; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Properties; + +@ExtendedParameters( + includeParametersInUsage = true, + commandName = "baksmali", + postfixDescription = "See baksmali help <command> for more information about a specific command") +public class Main extends Command { + public static final String VERSION = loadVersion(); + + @Parameter(names = {"--help", "-h", "-?"}, help = true, + description = "Show usage information") + private boolean help; + + @Parameter(names = {"--version", "-v"}, help = true, + description = "Print the version of baksmali and then exit") + public boolean version; + + private JCommander jc; + + public Main() { + super(Lists.<JCommander>newArrayList()); + } + + @Override public void run() { + } + + @Override protected JCommander getJCommander() { + return jc; + } + + public static void main(String[] args) { + Main main = new Main(); + + JCommander jc = new JCommander(main); + main.jc = jc; + jc.setProgramName("baksmali"); + List<JCommander> commandHierarchy = main.getCommandHierarchy(); + + ExtendedCommands.addExtendedCommand(jc, new DisassembleCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new DeodexCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new DumpCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new HelpCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new HlepCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new ListCommand(commandHierarchy)); + + jc.parse(args); + + if (main.version) { + version(); + } + + if (jc.getParsedCommand() == null || main.help) { + main.usage(); + return; + } + + Command command = (Command)jc.getCommands().get(jc.getParsedCommand()).getObjects().get(0); + command.run(); + } + + protected static void version() { + System.out.println("baksmali " + VERSION + " (http://smali.org)"); + System.out.println("Copyright (C) 2010 Ben Gruver (JesusFreke@JesusFreke.com)"); + System.out.println("BSD license (http://www.opensource.org/licenses/bsd-license.php)"); + System.exit(0); + } + + private static String loadVersion() { + InputStream propertiesStream = Baksmali.class.getClassLoader().getResourceAsStream("baksmali.properties"); + String version = "[unknown version]"; + if (propertiesStream != null) { + Properties properties = new Properties(); + try { + properties.load(propertiesStream); + version = properties.getProperty("application.version"); + } catch (IOException ex) { + // ignore + } + } + return version; + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/dump.java b/baksmali/src/main/java/org/jf/baksmali/dump.java deleted file mode 100644 index 79405e59..00000000 --- a/baksmali/src/main/java/org/jf/baksmali/dump.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * [The "BSD licence"] - * Copyright (c) 2010 Ben Gruver (JesusFreke) - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.jf.baksmali; - -import org.jf.dexlib2.Opcodes; -import org.jf.dexlib2.dexbacked.DexBackedDexFile; -import org.jf.dexlib2.dexbacked.raw.RawDexFile; -import org.jf.dexlib2.dexbacked.raw.util.DexAnnotator; -import org.jf.util.ConsoleUtil; - -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; - -public class dump { - public static void dump(DexBackedDexFile dexFile, String dumpFileName, int apiLevel) throws IOException { - if (dumpFileName != null) { - Writer writer = null; - - try { - writer = new BufferedWriter(new FileWriter(dumpFileName)); - - int consoleWidth = ConsoleUtil.getConsoleWidth(); - if (consoleWidth <= 0) { - consoleWidth = 120; - } - - RawDexFile rawDexFile = new RawDexFile(Opcodes.forApi(apiLevel), dexFile); - DexAnnotator annotator = new DexAnnotator(rawDexFile, consoleWidth); - annotator.writeAnnotations(writer); - } catch (IOException ex) { - System.err.println("There was an error while dumping the dex file to " + dumpFileName); - ex.printStackTrace(System.err); - } finally { - if (writer != null) { - try { - writer.close(); - } catch (IOException ex) { - System.err.println("There was an error while closing the dump file " + dumpFileName); - ex.printStackTrace(System.err); - } - } - } - } - } -} diff --git a/baksmali/src/main/java/org/jf/baksmali/main.java b/baksmali/src/main/java/org/jf/baksmali/main.java deleted file mode 100644 index 86f43d82..00000000 --- a/baksmali/src/main/java/org/jf/baksmali/main.java +++ /dev/null @@ -1,626 +0,0 @@ -/* - * [The "BSD licence"] - * Copyright (c) 2010 Ben Gruver (JesusFreke) - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.jf.baksmali; - -import com.google.common.collect.Lists; -import org.apache.commons.cli.*; -import org.jf.dexlib2.DexFileFactory; -import org.jf.dexlib2.DexFileFactory.MultipleDexFilesException; -import org.jf.dexlib2.analysis.InlineMethodResolver; -import org.jf.dexlib2.dexbacked.DexBackedDexFile; -import org.jf.dexlib2.dexbacked.DexBackedOdexFile; -import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; -import org.jf.dexlib2.iface.DexFile; -import org.jf.util.ConsoleUtil; -import org.jf.util.SmaliHelpFormatter; - -import javax.annotation.Nonnull; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Locale; -import java.util.Properties; - -public class main { - - public static final String VERSION; - - private static final Options basicOptions; - private static final Options debugOptions; - private static final Options options; - - static { - options = new Options(); - basicOptions = new Options(); - debugOptions = new Options(); - buildOptions(); - - InputStream templateStream = baksmali.class.getClassLoader().getResourceAsStream("baksmali.properties"); - if (templateStream != null) { - Properties properties = new Properties(); - String version = "(unknown)"; - try { - properties.load(templateStream); - version = properties.getProperty("application.version"); - } catch (IOException ex) { - // ignore - } - VERSION = version; - } else { - VERSION = "[unknown version]"; - } - } - - /** - * This class is uninstantiable. - */ - private main() { - } - - /** - * A more programmatic-friendly entry point for baksmali - * - * @param options a baksmaliOptions object with the options to run baksmali with - * @param inputDexFile The DexFile to disassemble - * @return true if disassembly completed with no errors, or false if errors were encountered - */ - public static boolean run(@Nonnull baksmaliOptions options, @Nonnull DexFile inputDexFile) throws IOException { - if (options.bootClassPathEntries.isEmpty() && - (options.deodex || options.registerInfo != 0 || options.normalizeVirtualMethods)) { - if (inputDexFile instanceof DexBackedOdexFile) { - options.bootClassPathEntries = ((DexBackedOdexFile)inputDexFile).getDependencies(); - } else { - options.bootClassPathEntries = getDefaultBootClassPathForApi(options.apiLevel, - options.experimental); - } - } - - if (options.customInlineDefinitions == null && inputDexFile instanceof DexBackedOdexFile) { - options.inlineResolver = - InlineMethodResolver.createInlineMethodResolver( - ((DexBackedOdexFile)inputDexFile).getOdexVersion()); - } - - boolean errorOccurred = false; - if (options.disassemble) { - errorOccurred = !baksmali.disassembleDexFile(inputDexFile, options); - } - - if (options.dump) { - if (!(inputDexFile instanceof DexBackedDexFile)) { - throw new IllegalArgumentException("Annotated hex-dumps require a DexBackedDexFile"); - } - dump.dump((DexBackedDexFile)inputDexFile, options.dumpFileName, options.apiLevel); - } - - return !errorOccurred; - } - - /** - * Run! - */ - public static void main(String[] args) throws IOException { - Locale locale = new Locale("en", "US"); - Locale.setDefault(locale); - - CommandLineParser parser = new PosixParser(); - CommandLine commandLine; - - try { - commandLine = parser.parse(options, args); - } catch (ParseException ex) { - usage(); - return; - } - - baksmaliOptions options = new baksmaliOptions(); - - String[] remainingArgs = commandLine.getArgs(); - Option[] clOptions = commandLine.getOptions(); - - for (int i=0; i<clOptions.length; i++) { - Option option = clOptions[i]; - String opt = option.getOpt(); - - switch (opt.charAt(0)) { - case 'v': - version(); - return; - case '?': - while (++i < clOptions.length) { - if (clOptions[i].getOpt().charAt(0) == '?') { - usage(true); - return; - } - } - usage(false); - return; - case 'o': - options.outputDirectory = commandLine.getOptionValue("o"); - break; - case 'p': - options.noParameterRegisters = true; - break; - case 'l': - options.useLocalsDirective = true; - break; - case 's': - options.useSequentialLabels = true; - break; - case 'b': - options.outputDebugInfo = false; - break; - case 'd': - options.bootClassPathDirs.add(option.getValue()); - break; - case 'f': - options.addCodeOffsets = true; - break; - case 'r': - String[] values = commandLine.getOptionValues('r'); - int registerInfo = 0; - - if (values == null || values.length == 0) { - registerInfo = baksmaliOptions.ARGS | baksmaliOptions.DEST; - } else { - for (String value: values) { - if (value.equalsIgnoreCase("ALL")) { - registerInfo |= baksmaliOptions.ALL; - } else if (value.equalsIgnoreCase("ALLPRE")) { - registerInfo |= baksmaliOptions.ALLPRE; - } else if (value.equalsIgnoreCase("ALLPOST")) { - registerInfo |= baksmaliOptions.ALLPOST; - } else if (value.equalsIgnoreCase("ARGS")) { - registerInfo |= baksmaliOptions.ARGS; - } else if (value.equalsIgnoreCase("DEST")) { - registerInfo |= baksmaliOptions.DEST; - } else if (value.equalsIgnoreCase("MERGE")) { - registerInfo |= baksmaliOptions.MERGE; - } else if (value.equalsIgnoreCase("FULLMERGE")) { - registerInfo |= baksmaliOptions.FULLMERGE; - } else { - usage(); - return; - } - } - - if ((registerInfo & baksmaliOptions.FULLMERGE) != 0) { - registerInfo &= ~baksmaliOptions.MERGE; - } - } - options.registerInfo = registerInfo; - break; - case 'c': - String bcp = commandLine.getOptionValue("c"); - if (bcp != null && bcp.charAt(0) == ':') { - options.addExtraClassPath(bcp); - } else { - options.setBootClassPath(bcp); - } - break; - case 'x': - options.deodex = true; - break; - case 'X': - options.experimental = true; - break; - case 'm': - options.noAccessorComments = true; - break; - case 'a': - options.apiLevel = Integer.parseInt(commandLine.getOptionValue("a")); - break; - case 'j': - options.jobs = Integer.parseInt(commandLine.getOptionValue("j")); - break; - case 'i': - String rif = commandLine.getOptionValue("i"); - options.setResourceIdFiles(rif); - break; - case 't': - options.useImplicitReferences = true; - break; - case 'e': - options.dexEntry = commandLine.getOptionValue("e"); - break; - case 'k': - options.checkPackagePrivateAccess = true; - break; - case 'n': - options.normalizeVirtualMethods = true; - break; - case 'N': - options.disassemble = false; - break; - case 'D': - options.dump = true; - options.dumpFileName = commandLine.getOptionValue("D"); - break; - case 'I': - options.ignoreErrors = true; - break; - case 'T': - options.customInlineDefinitions = new File(commandLine.getOptionValue("T")); - break; - default: - assert false; - } - } - - if (remainingArgs.length != 1) { - usage(); - return; - } - - String inputDexPath = remainingArgs[0]; - File dexFileFile = new File(inputDexPath); - if (!dexFileFile.exists()) { - System.err.println("Can't find the file " + inputDexPath); - System.exit(1); - } - - //Read in and parse the dex file - DexBackedDexFile dexFile = null; - try { - dexFile = DexFileFactory.loadDexFile(dexFileFile, options.dexEntry, options.apiLevel, options.experimental); - } catch (MultipleDexFilesException ex) { - System.err.println(String.format("%s contains multiple dex files. You must specify which one to " + - "disassemble with the -e option", dexFileFile.getName())); - System.err.println("Valid entries include:"); - - for (OatDexFile oatDexFile: ex.oatFile.getDexFiles()) { - System.err.println(oatDexFile.filename); - } - System.exit(1); - } - - if (dexFile.hasOdexOpcodes()) { - if (!options.deodex) { - System.err.println("Warning: You are disassembling an odex file without deodexing it. You"); - System.err.println("won't be able to re-assemble the results unless you deodex it with the -x"); - System.err.println("option"); - options.allowOdex = true; - } - } else { - options.deodex = false; - } - - if (options.dump) { - if (options.dumpFileName == null) { - options.dumpFileName = inputDexPath + ".dump"; - } - } - - try { - if (!run(options, dexFile)) { - System.exit(1); - } - } catch (IllegalArgumentException ex) { - System.err.println(ex.getMessage()); - System.exit(1); - } - } - - /** - * Prints the usage message. - */ - private static void usage(boolean printDebugOptions) { - SmaliHelpFormatter formatter = new SmaliHelpFormatter(); - int consoleWidth = ConsoleUtil.getConsoleWidth(); - if (consoleWidth <= 0) { - consoleWidth = 80; - } - - formatter.setWidth(consoleWidth); - - formatter.printHelp("java -jar baksmali.jar [options] <dex-file>", - "disassembles and/or dumps a dex file", basicOptions, printDebugOptions?debugOptions:null); - } - - private static void usage() { - usage(false); - } - - /** - * Prints the version message. - */ - protected static void version() { - System.out.println("baksmali " + VERSION + " (http://smali.googlecode.com)"); - System.out.println("Copyright (C) 2010 Ben Gruver (JesusFreke@JesusFreke.com)"); - System.out.println("BSD license (http://www.opensource.org/licenses/bsd-license.php)"); - System.exit(0); - } - - @SuppressWarnings("AccessStaticViaInstance") - private static void buildOptions() { - Option versionOption = OptionBuilder.withLongOpt("version") - .withDescription("prints the version then exits") - .create("v"); - - Option helpOption = OptionBuilder.withLongOpt("help") - .withDescription("prints the help message then exits. Specify twice for debug options") - .create("?"); - - Option outputDirOption = OptionBuilder.withLongOpt("output") - .withDescription("the directory where the disassembled files will be placed. The default is out") - .hasArg() - .withArgName("DIR") - .create("o"); - - Option noParameterRegistersOption = OptionBuilder.withLongOpt("no-parameter-registers") - .withDescription("use the v<n> syntax instead of the p<n> syntax for registers mapped to method " + - "parameters") - .create("p"); - - Option deodexerantOption = OptionBuilder.withLongOpt("deodex") - .withDescription("deodex the given odex file. This option is ignored if the input file is not an " + - "odex file") - .create("x"); - - Option experimentalOption = OptionBuilder.withLongOpt("experimental") - .withDescription("enable experimental opcodes to be disassembled, even if they aren't necessarily supported in the Android runtime yet") - .create("X"); - - Option useLocalsOption = OptionBuilder.withLongOpt("use-locals") - .withDescription("output the .locals directive with the number of non-parameter registers, rather" + - " than the .register directive with the total number of register") - .create("l"); - - Option sequentialLabelsOption = OptionBuilder.withLongOpt("sequential-labels") - .withDescription("create label names using a sequential numbering scheme per label type, rather than " + - "using the bytecode address") - .create("s"); - - Option noDebugInfoOption = OptionBuilder.withLongOpt("no-debug-info") - .withDescription("don't write out debug info (.local, .param, .line, etc.)") - .create("b"); - - Option registerInfoOption = OptionBuilder.withLongOpt("register-info") - .hasOptionalArgs() - .withArgName("REGISTER_INFO_TYPES") - .withValueSeparator(',') - .withDescription("print the specificed type(s) of register information for each instruction. " + - "\"ARGS,DEST\" is the default if no types are specified.\nValid values are:\nALL: all " + - "pre- and post-instruction registers.\nALLPRE: all pre-instruction registers\nALLPOST: all " + - "post-instruction registers\nARGS: any pre-instruction registers used as arguments to the " + - "instruction\nDEST: the post-instruction destination register, if any\nMERGE: Any " + - "pre-instruction register has been merged from more than 1 different post-instruction " + - "register from its predecessors\nFULLMERGE: For each register that would be printed by " + - "MERGE, also show the incoming register types that were merged") - .create("r"); - - Option classPathOption = OptionBuilder.withLongOpt("bootclasspath") - .withDescription("A colon-separated list of bootclasspath jar/oat files to use for analysis. Add an " + - "initial colon to specify that the jars/oats should be appended to the default bootclasspath " + - "instead of replacing it") - .hasOptionalArg() - .withArgName("BOOTCLASSPATH") - .create("c"); - - Option classPathDirOption = OptionBuilder.withLongOpt("bootclasspath-dir") - .withDescription("the base folder to look for the bootclasspath files in. Defaults to the current " + - "directory") - .hasArg() - .withArgName("DIR") - .create("d"); - - Option codeOffsetOption = OptionBuilder.withLongOpt("code-offsets") - .withDescription("add comments to the disassembly containing the code offset for each address") - .create("f"); - - Option noAccessorCommentsOption = OptionBuilder.withLongOpt("no-accessor-comments") - .withDescription("don't output helper comments for synthetic accessors") - .create("m"); - - Option apiLevelOption = OptionBuilder.withLongOpt("api-level") - .withDescription("The numeric api-level of the file being disassembled. If not " + - "specified, it defaults to 15 (ICS).") - .hasArg() - .withArgName("API_LEVEL") - .create("a"); - - Option jobsOption = OptionBuilder.withLongOpt("jobs") - .withDescription("The number of threads to use. Defaults to the number of cores available, up to a " + - "maximum of 6") - .hasArg() - .withArgName("NUM_THREADS") - .create("j"); - - Option resourceIdFilesOption = OptionBuilder.withLongOpt("resource-id-files") - .withDescription("the resource ID files to use, for analysis. A colon-separated list of prefix=file " + - "pairs. For example R=res/values/public.xml:" + - "android.R=$ANDROID_HOME/platforms/android-19/data/res/values/public.xml") - .hasArg() - .withArgName("FILES") - .create("i"); - - Option noImplicitReferencesOption = OptionBuilder.withLongOpt("implicit-references") - .withDescription("Use implicit (type-less) method and field references") - .create("t"); - - Option checkPackagePrivateAccessOption = OptionBuilder.withLongOpt("check-package-private-access") - .withDescription("When deodexing, use the package-private access check when calculating vtable " + - "indexes. It should only be needed for 4.2.0 odexes. The functionality was reverted for " + - "4.2.1.") - .create("k"); - - Option normalizeVirtualMethods = OptionBuilder.withLongOpt("normalize-virtual-methods") - .withDescription("Normalize virtual method references to the reference the base method.") - .create("n"); - - Option dumpOption = OptionBuilder.withLongOpt("dump-to") - .withDescription("dumps the given dex file into a single annotated dump file named FILE" + - " (<dexfile>.dump by default), along with the normal disassembly") - .hasOptionalArg() - .withArgName("FILE") - .create("D"); - - Option ignoreErrorsOption = OptionBuilder.withLongOpt("ignore-errors") - .withDescription("ignores any non-fatal errors that occur while disassembling/deodexing," + - " ignoring the class if needed, and continuing with the next class. The default" + - " behavior is to stop disassembling and exit once an error is encountered") - .create("I"); - - Option noDisassemblyOption = OptionBuilder.withLongOpt("no-disassembly") - .withDescription("suppresses the output of the disassembly") - .create("N"); - - Option inlineTableOption = OptionBuilder.withLongOpt("inline-table") - .withDescription("specify a file containing a custom inline method table to use for deodexing") - .hasArg() - .withArgName("FILE") - .create("T"); - - Option dexEntryOption = OptionBuilder.withLongOpt("dex-file") - .withDescription("looks for dex file named DEX_FILE, defaults to classes.dex") - .withArgName("DEX_FILE") - .hasArg() - .create("e"); - - basicOptions.addOption(versionOption); - basicOptions.addOption(helpOption); - basicOptions.addOption(outputDirOption); - basicOptions.addOption(noParameterRegistersOption); - basicOptions.addOption(deodexerantOption); - basicOptions.addOption(experimentalOption); - basicOptions.addOption(useLocalsOption); - basicOptions.addOption(sequentialLabelsOption); - basicOptions.addOption(noDebugInfoOption); - basicOptions.addOption(registerInfoOption); - basicOptions.addOption(classPathOption); - basicOptions.addOption(classPathDirOption); - basicOptions.addOption(codeOffsetOption); - basicOptions.addOption(noAccessorCommentsOption); - basicOptions.addOption(apiLevelOption); - basicOptions.addOption(jobsOption); - basicOptions.addOption(resourceIdFilesOption); - basicOptions.addOption(noImplicitReferencesOption); - basicOptions.addOption(dexEntryOption); - basicOptions.addOption(checkPackagePrivateAccessOption); - basicOptions.addOption(normalizeVirtualMethods); - - debugOptions.addOption(dumpOption); - debugOptions.addOption(ignoreErrorsOption); - debugOptions.addOption(noDisassemblyOption); - debugOptions.addOption(inlineTableOption); - - for (Object option: basicOptions.getOptions()) { - options.addOption((Option)option); - } - for (Object option: debugOptions.getOptions()) { - options.addOption((Option)option); - } - } - - @Nonnull - private static List<String> getDefaultBootClassPathForApi(int apiLevel, boolean experimental) { - if (apiLevel < 9) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar"); - } else if (apiLevel < 12) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/core-junit.jar"); - } else if (apiLevel < 14) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/apache-xml.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/core-junit.jar"); - } else if (apiLevel < 16) { - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/core-junit.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/apache-xml.jar", - "/system/framework/filterfw.jar"); - } else if (apiLevel < 21) { - // this is correct as of api 17/4.2.2 - return Lists.newArrayList( - "/system/framework/core.jar", - "/system/framework/core-junit.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/telephony-common.jar", - "/system/framework/mms-common.jar", - "/system/framework/android.policy.jar", - "/system/framework/services.jar", - "/system/framework/apache-xml.jar"); - } else if (apiLevel < 26) { - return Lists.newArrayList( - "/system/framework/core-libart.jar", - "/system/framework/conscrypt.jar", - "/system/framework/okhttp.jar", - "/system/framework/core-junit.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/telephony-common.jar", - "/system/framework/voip-common.jar", - "/system/framework/ims-common.jar", - "/system/framework/mms-common.jar", - "/system/framework/android.policy.jar", - "/system/framework/apache-xml.jar"); - } else { // api >= 26 - // TODO: verify, add new ones? - return Lists.newArrayList( - "/system/framework/core-libart.jar", - "/system/framework/conscrypt.jar", - "/system/framework/okhttp.jar", - "/system/framework/bouncycastle.jar", - "/system/framework/ext.jar", - "/system/framework/framework.jar", - "/system/framework/telephony-common.jar", - "/system/framework/voip-common.jar", - "/system/framework/ims-common.jar", - "/system/framework/mms-common.jar", - "/system/framework/android.policy.jar", - "/system/framework/apache-xml.jar"); - } - } -} diff --git a/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java b/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java index 2bb04dda..a68038dc 100644 --- a/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java +++ b/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java @@ -36,7 +36,9 @@ import com.google.common.io.Resources; import junit.framework.Assert; import org.jf.baksmali.Adaptors.ClassDefinition; import org.jf.dexlib2.DexFileFactory; +import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.analysis.ClassPath; +import org.jf.dexlib2.analysis.ClassProvider; import org.jf.dexlib2.iface.ClassDef; import org.jf.dexlib2.iface.DexFile; import org.jf.util.IndentingWriter; @@ -49,6 +51,7 @@ import java.io.IOException; import java.io.StringWriter; import java.net.URISyntaxException; import java.net.URL; +import java.util.ArrayList; public class AnalysisTest { @@ -68,6 +71,11 @@ public class AnalysisTest { } @Test + public void InstanceOfTest() throws IOException, URISyntaxException { + runTest("InstanceOfTest", true, true); + } + + @Test public void MultipleStartInstructionsTest() throws IOException, URISyntaxException { runTest("MultipleStartInstructionsTest", true); } @@ -83,16 +91,24 @@ public class AnalysisTest { } public void runTest(String test, boolean registerInfo) throws IOException, URISyntaxException { + runTest(test, registerInfo, false); + } + + public void runTest(String test, boolean registerInfo, boolean isArt) throws IOException, URISyntaxException { String dexFilePath = String.format("%s%sclasses.dex", test, File.separatorChar); - DexFile dexFile = DexFileFactory.loadDexFile(findResource(dexFilePath), 15, false); + DexFile dexFile = DexFileFactory.loadDexFile(findResource(dexFilePath), Opcodes.getDefault()); - baksmaliOptions options = new baksmaliOptions(); + BaksmaliOptions options = new BaksmaliOptions(); if (registerInfo) { - options.registerInfo = baksmaliOptions.ALL | baksmaliOptions.FULLMERGE; - options.classPath = new ClassPath(); + options.registerInfo = BaksmaliOptions.ALL | BaksmaliOptions.FULLMERGE; + if (isArt) { + options.classPath = new ClassPath(new ArrayList<ClassProvider>(), true, 56); + } else { + options.classPath = new ClassPath(); + } } - options.useImplicitReferences = false; + options.implicitReferences = false; for (ClassDef classDef: dexFile.getClasses()) { StringWriter stringWriter = new StringWriter(); diff --git a/baksmali/src/test/java/org/jf/baksmali/BaksmaliTestUtils.java b/baksmali/src/test/java/org/jf/baksmali/BaksmaliTestUtils.java index 1c570b6c..4dd2ad93 100644 --- a/baksmali/src/test/java/org/jf/baksmali/BaksmaliTestUtils.java +++ b/baksmali/src/test/java/org/jf/baksmali/BaksmaliTestUtils.java @@ -48,10 +48,9 @@ import java.io.StringWriter; public class BaksmaliTestUtils { public static void assertSmaliCompiledEquals(String source, String expected, - baksmaliOptions options, boolean stripComments) throws IOException, + BaksmaliOptions options, boolean stripComments) throws IOException, RecognitionException { - ClassDef classDef = SmaliTestUtils.compileSmali(source, options.apiLevel, - options.experimental); + ClassDef classDef = SmaliTestUtils.compileSmali(source, options.apiLevel); // Remove unnecessary whitespace and optionally strip all comments from smali file String normalizedActual = getNormalizedSmali(classDef, options, stripComments); @@ -62,13 +61,13 @@ public class BaksmaliTestUtils { } public static void assertSmaliCompiledEquals(String source, String expected, - baksmaliOptions options) throws IOException, RecognitionException { + BaksmaliOptions options) throws IOException, RecognitionException { assertSmaliCompiledEquals(source, expected, options, false); } public static void assertSmaliCompiledEquals(String source, String expected) throws IOException, RecognitionException { - baksmaliOptions options = new baksmaliOptions(); + BaksmaliOptions options = new BaksmaliOptions(); assertSmaliCompiledEquals(source, expected, options); } @@ -81,7 +80,7 @@ public class BaksmaliTestUtils { } @Nonnull - public static String getNormalizedSmali(@Nonnull ClassDef classDef, @Nonnull baksmaliOptions options, + public static String getNormalizedSmali(@Nonnull ClassDef classDef, @Nonnull BaksmaliOptions options, boolean stripComments) throws IOException { StringWriter stringWriter = new StringWriter(); diff --git a/baksmali/src/test/java/org/jf/baksmali/DexTest.java b/baksmali/src/test/java/org/jf/baksmali/DexTest.java index 5a4db658..f9f55622 100644 --- a/baksmali/src/test/java/org/jf/baksmali/DexTest.java +++ b/baksmali/src/test/java/org/jf/baksmali/DexTest.java @@ -65,7 +65,7 @@ public abstract class DexTest { } @Nonnull - protected DexBackedDexFile getInputDexFile(@Nonnull String testName, @Nonnull baksmaliOptions options) { + protected DexBackedDexFile getInputDexFile(@Nonnull String testName, @Nonnull BaksmaliOptions options) { try { // Load file from resources as a stream byte[] inputBytes = BaksmaliTestUtils.readResourceBytesFully(getInputFilename(testName)); diff --git a/baksmali/src/test/java/org/jf/baksmali/DisassemblyTest.java b/baksmali/src/test/java/org/jf/baksmali/DisassemblyTest.java index 1a34e8c3..769372eb 100644 --- a/baksmali/src/test/java/org/jf/baksmali/DisassemblyTest.java +++ b/baksmali/src/test/java/org/jf/baksmali/DisassemblyTest.java @@ -57,10 +57,10 @@ public class DisassemblyTest extends DexTest { } protected void runTest(@Nonnull String testName) { - runTest(testName, new baksmaliOptions()); + runTest(testName, new BaksmaliOptions()); } - protected void runTest(@Nonnull String testName, @Nonnull baksmaliOptions options) { + protected void runTest(@Nonnull String testName, @Nonnull BaksmaliOptions options) { try { DexBackedDexFile inputDex = getInputDexFile(testName, options); Assert.assertEquals(1, inputDex.getClassCount()); diff --git a/baksmali/src/test/java/org/jf/baksmali/FieldGapOrderTest.java b/baksmali/src/test/java/org/jf/baksmali/FieldGapOrderTest.java index 78fabc0b..ad2aad5b 100644 --- a/baksmali/src/test/java/org/jf/baksmali/FieldGapOrderTest.java +++ b/baksmali/src/test/java/org/jf/baksmali/FieldGapOrderTest.java @@ -42,7 +42,7 @@ import org.junit.Test; public class FieldGapOrderTest extends DexTest { @Test public void testOldOrder() { - DexFile dexFile = getInputDexFile("FieldGapOrder", new baksmaliOptions()); + DexFile dexFile = getInputDexFile("FieldGapOrder", new BaksmaliOptions()); Assert.assertEquals(3, dexFile.getClasses().size()); ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), false, 66); @@ -56,7 +56,7 @@ public class FieldGapOrderTest extends DexTest { @Test public void testNewOrder() { - DexFile dexFile = getInputDexFile("FieldGapOrder", new baksmaliOptions()); + DexFile dexFile = getInputDexFile("FieldGapOrder", new BaksmaliOptions()); Assert.assertEquals(3, dexFile.getClasses().size()); ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), false, 67); diff --git a/baksmali/src/test/java/org/jf/baksmali/ImplicitReferenceTest.java b/baksmali/src/test/java/org/jf/baksmali/ImplicitReferenceTest.java index 1f2ae5bf..962a6be7 100644 --- a/baksmali/src/test/java/org/jf/baksmali/ImplicitReferenceTest.java +++ b/baksmali/src/test/java/org/jf/baksmali/ImplicitReferenceTest.java @@ -62,8 +62,8 @@ public class ImplicitReferenceTest { "return-void\n" + ".end method\n"; - baksmaliOptions options = new baksmaliOptions(); - options.useImplicitReferences = true; + BaksmaliOptions options = new BaksmaliOptions(); + options.implicitReferences = true; BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options); } @@ -93,8 +93,8 @@ public class ImplicitReferenceTest { " return-void\n" + ".end method\n"; - baksmaliOptions options = new baksmaliOptions(); - options.useImplicitReferences = false; + BaksmaliOptions options = new BaksmaliOptions(); + options.implicitReferences = false; BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options); } @@ -118,8 +118,8 @@ public class ImplicitReferenceTest { ".field public static field3:Ljava/lang/reflect/Method; = I()V\n" + ".field public static field4:Ljava/lang/Class; = I\n"; - baksmaliOptions options = new baksmaliOptions(); - options.useImplicitReferences = true; + BaksmaliOptions options = new BaksmaliOptions(); + options.implicitReferences = true; BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options); } @@ -143,8 +143,8 @@ public class ImplicitReferenceTest { ".field public static field3:Ljava/lang/reflect/Method; = LHelloWorld;->I()V\n" + ".field public static field4:Ljava/lang/Class; = I\n"; - baksmaliOptions options = new baksmaliOptions(); - options.useImplicitReferences = false; + BaksmaliOptions options = new BaksmaliOptions(); + options.implicitReferences = false; BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options); } @@ -174,8 +174,8 @@ public class ImplicitReferenceTest { " return-void\n" + ".end method\n"; - baksmaliOptions options = new baksmaliOptions(); - options.useImplicitReferences = true; + BaksmaliOptions options = new BaksmaliOptions(); + options.implicitReferences = true; BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options); } @@ -205,8 +205,8 @@ public class ImplicitReferenceTest { " return-void\n" + ".end method\n"; - baksmaliOptions options = new baksmaliOptions(); - options.useImplicitReferences = false; + BaksmaliOptions options = new BaksmaliOptions(); + options.implicitReferences = false; BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options); } @@ -228,8 +228,8 @@ public class ImplicitReferenceTest { ".field public static field2:Ljava/lang/reflect/Field; = V:I\n" + ".field public static field3:Ljava/lang/reflect/Field; = I:I\n"; - baksmaliOptions options = new baksmaliOptions(); - options.useImplicitReferences = true; + BaksmaliOptions options = new BaksmaliOptions(); + options.implicitReferences = true; BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options); } @@ -251,8 +251,8 @@ public class ImplicitReferenceTest { ".field public static field2:Ljava/lang/reflect/Field; = LHelloWorld;->V:I\n" + ".field public static field3:Ljava/lang/reflect/Field; = LHelloWorld;->I:I\n"; - baksmaliOptions options = new baksmaliOptions(); - options.useImplicitReferences = false; + BaksmaliOptions options = new BaksmaliOptions(); + options.implicitReferences = false; BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options); } diff --git a/baksmali/src/test/java/org/jf/baksmali/InterfaceOrderTest.java b/baksmali/src/test/java/org/jf/baksmali/InterfaceOrderTest.java index d85d7913..f1ade1e9 100644 --- a/baksmali/src/test/java/org/jf/baksmali/InterfaceOrderTest.java +++ b/baksmali/src/test/java/org/jf/baksmali/InterfaceOrderTest.java @@ -36,6 +36,6 @@ import org.junit.Test; public class InterfaceOrderTest extends IdenticalRoundtripTest { @Test public void testInterfaceOrder() { - runTest("InterfaceOrder", new baksmaliOptions()); + runTest("InterfaceOrder", new BaksmaliOptions()); } } diff --git a/baksmali/src/test/java/org/jf/baksmali/RoundtripTest.java b/baksmali/src/test/java/org/jf/baksmali/RoundtripTest.java index c9ff2d4d..81e98a30 100644 --- a/baksmali/src/test/java/org/jf/baksmali/RoundtripTest.java +++ b/baksmali/src/test/java/org/jf/baksmali/RoundtripTest.java @@ -69,10 +69,10 @@ public abstract class RoundtripTest { } protected void runTest(@Nonnull String testName) { - runTest(testName, new baksmaliOptions()); + runTest(testName, new BaksmaliOptions()); } - protected void runTest(@Nonnull String testName, @Nonnull baksmaliOptions options) { + protected void runTest(@Nonnull String testName, @Nonnull BaksmaliOptions options) { try { // Load file from resources as a stream String inputFilename = getInputFilename(testName); diff --git a/baksmali/src/test/resources/InstanceOfTest/InstanceOfTest.smali b/baksmali/src/test/resources/InstanceOfTest/InstanceOfTest.smali new file mode 100644 index 00000000..8e3337af --- /dev/null +++ b/baksmali/src/test/resources/InstanceOfTest/InstanceOfTest.smali @@ -0,0 +1,118 @@ +.class public LInstanceOfTest; +.super Ljava/lang/Object; + + +# virtual methods +.method public testInstanceOfEqz(Ljava/lang/Object;)I + .registers 3 + + #v0=(Uninit);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + instance-of v0, p1, Ljava/lang/String; + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + if-eqz v0, :cond_9 + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Unknown); + + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + invoke-virtual {p1}, Ljava/lang/String;->length()I + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + move-result v0 + #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + + #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + return v0 + #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + + :cond_9 + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + const v0, -0x1 + #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + + #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + return v0 + #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); +.end method + +.method public testInstanceOfNez(Ljava/lang/Object;)I + .registers 3 + + #v0=(Uninit);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + instance-of v0, p1, Ljava/lang/String; + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + if-nez v0, :cond_8 + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Unknown); + + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + const v0, -0x1 + #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + + #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + return v0 + #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + + :cond_8 + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + invoke-virtual {p1}, Ljava/lang/String;->length()I + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + + #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + move-result v0 + #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + + #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); + return v0 + #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;); +.end method + +.method public testRegisterAlias(Ljava/lang/Object;)I + .registers 4 + + #v0=(Uninit);v1=(Uninit);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;); + move-object p0, p1 + #v0=(Uninit);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;); + + #v0=(Uninit);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;); + instance-of v0, p0, Ljava/lang/String; + #v0=(Boolean);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;); + + #v0=(Boolean);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;); + if-eqz v0, :cond_f + #v0=(Boolean);v1=(Uninit);p0=(Unknown);p1=(Unknown); + + :cond_5 + #v0=(Integer):merge{0x3:(Boolean),0xc:(Integer)} + #v1=(Conflicted):merge{0x3:(Uninit),0xc:(Null)} + #p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + invoke-virtual {p1}, Ljava/lang/String;->length()I + #v0=(Integer);v1=(Conflicted);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + + #v0=(Integer);v1=(Conflicted);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + move-result v0 + #v0=(Integer);v1=(Conflicted);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + + #v0=(Integer);v1=(Conflicted);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + const v1, 0x0 + #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + + #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + if-le v0, v1, :cond_5 + #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + + #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + return v0 + #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;); + + :cond_f + #v0=(Boolean);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;); + const v0, -0x1 + #v0=(Byte);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;); + + #v0=(Byte);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;); + return v0 + #v0=(Byte);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;); +.end method diff --git a/baksmali/src/test/resources/InstanceOfTest/classes.dex b/baksmali/src/test/resources/InstanceOfTest/classes.dex Binary files differnew file mode 100644 index 00000000..571bdb8a --- /dev/null +++ b/baksmali/src/test/resources/InstanceOfTest/classes.dex diff --git a/baksmali/src/test/resources/UninitRefIdentityTest/UninitRefIdentityTest.smali b/baksmali/src/test/resources/UninitRefIdentityTest/UninitRefIdentityTest.smali index f9c43637..1970d3b4 100644 --- a/baksmali/src/test/resources/UninitRefIdentityTest/UninitRefIdentityTest.smali +++ b/baksmali/src/test/resources/UninitRefIdentityTest/UninitRefIdentityTest.smali @@ -26,11 +26,37 @@ #v0=(Conflicted):merge{0x5:(UninitRef,Ljava/lang/String;),0x7:(UninitRef,Ljava/lang/String;)} #v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;); invoke-direct {v0}, Ljava/lang/String;-><init>()V - #v0=(Unknown);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;); + #v0=(Conflicted);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;); - #v0=(Unknown);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;); + #v0=(Conflicted);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;); return-void - #v0=(Unknown);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;); + #v0=(Conflicted);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;); +.end method + +.method public constructor <init>(I)V + .registers 2 + + #p0=(UninitThis,LUninitRefIdentityTest;);p1=(Integer); + move-object p1, p0 + #p0=(UninitThis,LUninitRefIdentityTest;);p1=(UninitThis,LUninitRefIdentityTest;); + + #p0=(UninitThis,LUninitRefIdentityTest;);p1=(UninitThis,LUninitRefIdentityTest;); + invoke-direct {p1}, Ljava/lang/Object;-><init>()V + #p0=(Reference,LUninitRefIdentityTest;);p1=(Reference,LUninitRefIdentityTest;); + + :cond_4 + #p0=(Reference,LUninitRefIdentityTest;); + #p1=(Reference,LUninitRefIdentityTest;):merge{0x1:(Reference,LUninitRefIdentityTest;),0x7:(Null)} + const p1, 0x0 + #p0=(Reference,LUninitRefIdentityTest;);p1=(Null); + + #p0=(Reference,LUninitRefIdentityTest;);p1=(Null); + if-nez p1, :cond_4 + #p0=(Reference,LUninitRefIdentityTest;);p1=(Null); + + #p0=(Reference,LUninitRefIdentityTest;);p1=(Null); + return-void + #p0=(Reference,LUninitRefIdentityTest;);p1=(Null); .end method .method public constructor <init>(Ljava/lang/String;)V @@ -48,3 +74,37 @@ return-void #p0=(Reference,LUninitRefIdentityTest;);p1=(Reference,LUninitRefIdentityTest;); .end method + + +# virtual methods +.method public overlappingInits()V + .registers 3 + + #v0=(Uninit);v1=(Uninit);p0=(Reference,LUninitRefIdentityTest;); + new-instance v0, Ljava/lang/String; + #v0=(UninitRef,Ljava/lang/String;);v1=(Uninit);p0=(Reference,LUninitRefIdentityTest;); + + #v0=(UninitRef,Ljava/lang/String;);v1=(Uninit);p0=(Reference,LUninitRefIdentityTest;); + new-instance v1, Ljava/lang/String; + #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(Reference,LUninitRefIdentityTest;); + + #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(Reference,LUninitRefIdentityTest;); + new-instance p0, Ljava/lang/String; + #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(UninitRef,Ljava/lang/String;); + + #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(UninitRef,Ljava/lang/String;); + invoke-direct {p0}, Ljava/lang/String;-><init>()V + #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;); + + #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;); + invoke-direct {v1}, Ljava/lang/String;-><init>()V + #v0=(UninitRef,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;); + + #v0=(UninitRef,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;); + invoke-direct {v0}, Ljava/lang/String;-><init>()V + #v0=(Reference,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;); + + #v0=(Reference,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;); + return-void + #v0=(Reference,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;); +.end method diff --git a/baksmali/src/test/resources/UninitRefIdentityTest/classes.dex b/baksmali/src/test/resources/UninitRefIdentityTest/classes.dex Binary files differindex ea146cf6..0f0caab3 100644 --- a/baksmali/src/test/resources/UninitRefIdentityTest/classes.dex +++ b/baksmali/src/test/resources/UninitRefIdentityTest/classes.dex diff --git a/build.gradle b/build.gradle index 56aaa3f6..9e8205d4 100644 --- a/build.gradle +++ b/build.gradle @@ -31,9 +31,14 @@ apply plugin: 'idea' -version = '2.1.3' +version = '2.2.0' +def jcommanderVersion = '' if (!('release' in gradle.startParameter.taskNames)) { + // we compile against 1.48 normally, to match what's in AOSP, but switch to a newer version + // for release, because it has some fixes required when running on Android + jcommanderVersion = 'com.beust:jcommander:1.48' + def versionSuffix try { def git = org.eclipse.jgit.api.Git.open(file('.')) @@ -51,6 +56,8 @@ if (!('release' in gradle.startParameter.taskNames)) { version += "-${versionSuffix}" } else { + jcommanderVersion = 'com.beust:jcommander:1.64' + if (System.env.JDK6_HOME == null && !JavaVersion.current().isJava6()) { throw new InvalidUserDataException("bzzzzzzzt. Release builds must be performed with java 6. " + "Either run gradle with java 6, or define the JDK6_HOME environment variable.") @@ -101,15 +108,16 @@ subprojects { guava: 'com.google.guava:guava:18.0', findbugs: 'com.google.code.findbugs:jsr305:1.3.9', junit: 'junit:junit:4.6', + mockito: 'org.mockito:mockito-core:1.10.19', antlr_runtime: 'org.antlr:antlr-runtime:3.5.2', antlr: 'org.antlr:antlr:3.5.2', stringtemplate: 'org.antlr:stringtemplate:3.2.1', - commons_cli: 'commons-cli:commons-cli:1.2', jflex_plugin: 'org.xbib.gradle.plugin:gradle-plugin-jflex:1.1.0', proguard_gradle: 'net.sf.proguard:proguard-gradle:5.2.1', dx: 'com.google.android.tools:dx:1.7', - gson: 'com.google.code.gson:gson:2.3.1' - ] + gson: 'com.google.code.gson:gson:2.3.1', + jcommander: jcommanderVersion + ] } repositories { @@ -196,5 +204,6 @@ buildscript { } task wrapper(type: Wrapper) { - gradleVersion = '2.14' + gradleVersion = '3.1' + distributionType = Wrapper.DistributionType.ALL }
\ No newline at end of file diff --git a/dexlib2/OatVersions.txt b/dexlib2/OatVersions.txt index 8aa9ea96..329c4f04 100644 --- a/dexlib2/OatVersions.txt +++ b/dexlib2/OatVersions.txt @@ -8,6 +8,7 @@ d7cbf8a6629942e7bd315ffae7e1c77b082f3e11 - 60 - return-void-barrier -> return-void-no-barrier 1412dfa4adcd511902e510fa0c948b168ab5840c - 61 (re-commit of f3251d12) 9d6bf69ad3012a9d843268fdd5325b6719b6d5f2 - 62 +- classpath list was added 0de1133ba600f299b3d67938f650720d9f859eb2 - 63 07785bb98dc8bbe192970e0f4c2cafd338a8dc68 - 64 fa2c054b28d4b540c1b3651401a7a091282a015f - 65 @@ -21,4 +22,69 @@ fab6788358dfb64e5c370611ddbbbffab0ed0553 - 67 6e2d5747d00697a25251d25dd33b953e54709507 - 68 (revert of 54b62480) 0747466fca310eedea5fc49e37d54f240a0b3c0f - 69 (re-commit of 54b62480) 501fd635a557645ab05f893c56e1f358e21bab82 - 70 -99170c636dfae4908b102347cfe9f92bad1881cc - 71
\ No newline at end of file +99170c636dfae4908b102347cfe9f92bad1881cc - 71 +3cfa4d05afa76e19ca99ec964b535a15c73683f0 - 72 +- default methods +d9786b0e5be23ea0258405165098b4216579209c - 73 +- fast class lookup table +a4f1220c1518074db18ca1044e9201492975750b - 74 +625a64aad13905d8a2454bf3cc0e874487b110d5 - 75 +- bootclasspath list was added +- class offsets moved out to a separate table +919f5536182890d2e03f59b961acf8f7c836ff61 - 74 (revert of 625a64aa) +9bdf108885a27ba05fae8501725649574d7c491b - 75 (re-commit of 625a64aa) +a62d2f04a6ecf804f8a78e722a6ca8ccb2dfa931 - 76 +845e5064580bd37ad5014f7aa0d078be7265464d - 75 (revert of a62d2f04) +29d38e77c553c6cf71fc4dafe2d22b4e3f814872 - 76 (re-commit of 845e5064) +d1537b569b6cd18297c5e02d13cdd588c4366c51 - 77 +61b28a17d9b6e8e998103646e98e4a9772e11927 - 78 +9d07e3d128ccfa0ef7670feadd424a825e447d1d - 79 +952e1e3710158982941fc70326e9fddc3021235d - 80 +013e3f33495dcc31dba19c9de128d23ed441d7d3 - 81 +87f3fcbd0db352157fc59148e94647ef21b73bce - 82 +02b75806a80f8b75c3d6ba2ff97c995117630f36 - 83 +4359e61927866c254bc2d701e3ea4c48de10b79c - 84 +d549c28cfbddba945cb88857bcca3dce1414fb29 - 85 +952dbb19cd094b8bfb01dbb33e0878db429e499a - 86 +239d6eaff0cbb5c4c0139f7053a012758799f186 - 87 - introduction of vdex files +77d9dd75d5d4a22ad1235f9a08d2cfbf2f0ae6fa - 89 +af1e2990cd1406a0fb7cba1d2e208208e950e413 - 90 +9fd8c60cdff7b28a89bb97fd90ae9d0f37cf8f8b - 91 +6beced4c017826f7c449f12fac7fa42403657f2b - 92 +58c3f6a0d15a4340c0a11ab7fbc8c4b990c64b77 - 93 +5923b5238091d9cd65f988fc059deb4fbb2e7f08 - 94 +2b615ba29c4dfcf54aaf44955f2eac60f5080b2e - 95 +f7aaacd97881c6924b8212c7f8fe4a4c8721ef53 - 94 (revert of 2b615ba) +0d3998b5ff619364acf47bec0b541e7a49bd6fe7 - 95 (re-commit of 2b615ba) +ac141397dc29189ad2b2df41f8d4312246beec60 - 96 +1998cd02603197f2acdc0734397a6d48b2f59b80 - 97 +e71b35446985835363a4508646cf7b1121bd95a3 - 98 +39cee66a8ddf0254626c9591662cf87e4a1cedc4 - 99 +cc99df230feb46ba717252f002d0cc2da6828421 - 100 +fee255039e30c1c3dfc70c426c3d176221c3cdf9 - 99 (revert of cc99df23) +e761bccf9f0d884cc4d4ec104568cef968296492 - 100 (re-commit of cc99df23) +8d91ac31ccb92557e434d89ffade3372466e1af5 - 101 +fd3161acfbe82c54ef49958f0ccc62511f224f91 - 102 +a2f526f889be06f96ea59624c9dfb1223b3839f3 - 103 +b048cb74b742b03eb6dd5f1d6dd49e559f730b36 - 104 +12f1b99775bbf7dd82d0a897587ab6ed0e75ee22 - 105 +ec7862283dd49f5a58d0ac45960ce27c2f7671b8 - 106 +45aa598cd1773f5eb1705dec13bea059238e054d - 107 +d16363a93053de0f32252c7897d839a46aff14ae - 108 +1a20b6801f2432a42b906f0de01e7e9586526aec - 109 +575d3e60c68b5cf481b615dde4a16283507b19ed - 110 +85c0f2ac03417f5125bc2ff1dab8109859c67d5c - 111 +5812e20ff7cbc8efa0b8d7486ada2f58840a6ad5 - 111 +b7ea3799c15b0090bb690e18ac1b5b0fddbdeee8 - 112 + - version bump for missing bump in commits + - 3228908337fdfe851223f8ae374538de25cb5ad1 + - 5812e20ff7cbc8efa0b8d7486ada2f58840a6ad5 +d776ff08e07494327716f0d2ea1a774b2ebfbca9 - 113 +bfb80d25eaeb7a604d5dd25a370e3869e96a33ab - 114 +1aea3510b8dd0c512cec61c91c5ef1f1e5d53d64 - 115 +6374c58f2ea403b3a05fb27376110fe4d0fc8e3f - 114 (revert of 1aea3510) +0b66d6174bf1f6023f9d36dda8538490b79c2e9f - 113 (revert of bfb80d25) +8d6768d47b66a688d35399d524ad5a5450e9d9d4 - 114 (i don't even) +f44d36c8423f81cbb5e9f55d8813e26ffa1a7f3b - 115 (115 again. heck if I know what's going on) +cbcedbf9382bc773713cd3552ed96f417bf1daeb - 116 +051071718085ce807a2e7c55278a8d723e238e86 - 116 diff --git a/dexlib2/VdexVersions.txt b/dexlib2/VdexVersions.txt new file mode 100644 index 00000000..9cb1a731 --- /dev/null +++ b/dexlib2/VdexVersions.txt @@ -0,0 +1,8 @@ +7b49e6cade09bc65b3b5f22d45fc9d0a7184e4f2 - 0 - introduction of vdex files +5d5a36bddbc008cd52a3207aa2b31177c47f9a49 - 0 - verifier deps +4acefd33064d37b41ca55c3c9355345a20e5f9c2 - 0 - quickening info +f54e5df37cb42d9a83fc54b375da5ef335d604a9 - 1 - dex file count + dex location checksum +7498105ec7497bae2ba9f1a697da9efa0c979654 - 2 - verify profile +3eba863e41d531340392d9ec64e17963ac898d81 - 3 +97fa9928c07d3e0ee631235e9619fb0f8949ed7a - 4 +6e54f78c7c1e01c1a91a458c6e51cca1c7d13ad4 - 5 diff --git a/dexlib2/build.gradle b/dexlib2/build.gradle index 8fbe5ffe..422d2c31 100644 --- a/dexlib2/build.gradle +++ b/dexlib2/build.gradle @@ -51,6 +51,7 @@ dependencies { compile depends.guava testCompile depends.junit + testCompile depends.mockito accessorTestGenerator project('accessorTestGenerator') diff --git a/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java b/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java index 60488ba2..1caaf9f8 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java @@ -31,14 +31,22 @@ package org.jf.dexlib2; -import com.google.common.base.MoreObjects; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; +import com.google.common.io.Files; import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; import org.jf.dexlib2.dexbacked.DexBackedOdexFile; import org.jf.dexlib2.dexbacked.OatFile; import org.jf.dexlib2.dexbacked.OatFile.NotAnOatFileException; import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; +import org.jf.dexlib2.dexbacked.OatFile.VdexProvider; +import org.jf.dexlib2.dexbacked.ZipDexContainer; +import org.jf.dexlib2.dexbacked.ZipDexContainer.NotAZipFileException; import org.jf.dexlib2.iface.DexFile; +import org.jf.dexlib2.iface.MultiDexContainer; import org.jf.dexlib2.writer.pool.DexPool; import org.jf.util.ExceptionWithContext; @@ -46,80 +54,44 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.*; import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; public final class DexFileFactory { - @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull String path, int api) throws IOException { - return loadDexFile(path, api, false); - } - - @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull String path, int api, boolean experimental) - throws IOException { - return loadDexFile(new File(path), "classes.dex", Opcodes.forApi(api, experimental)); - } @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, int api) throws IOException { - return loadDexFile(dexFile, api, false); + public static DexBackedDexFile loadDexFile(@Nonnull String path, @Nonnull Opcodes opcodes) throws IOException { + return loadDexFile(new File(path), opcodes); } + /** + * Loads a dex/apk/odex/oat file. + * + * For oat files with multiple dex files, the first will be opened. For zip/apk files, the "classes.dex" entry + * will be opened. + * + * @param file The file to open + * @param opcodes The set of opcodes to use + * @return A DexBackedDexFile for the given file + * + * @throws UnsupportedOatVersionException If file refers to an unsupported oat file + * @throws DexFileNotFoundException If file does not exist, if file is a zip file but does not have a "classes.dex" + * entry, or if file is an oat file that has no dex entries. + * @throws UnsupportedFileTypeException If file is not a valid dex/zip/odex/oat file, or if the "classes.dex" entry + * in a zip file is not a valid dex file + */ @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, int api, boolean experimental) - throws IOException { - return loadDexFile(dexFile, null, Opcodes.forApi(api, experimental)); - } - - @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, @Nullable String dexEntry, int api, - boolean experimental) throws IOException { - return loadDexFile(dexFile, dexEntry, Opcodes.forApi(api, experimental)); - } + public static DexBackedDexFile loadDexFile(@Nonnull File file, @Nonnull Opcodes opcodes) throws IOException { + if (!file.exists()) { + throw new DexFileNotFoundException("%s does not exist", file.getName()); + } - @Nonnull - public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, @Nullable String dexEntry, - @Nonnull Opcodes opcodes) throws IOException { - ZipFile zipFile = null; - boolean isZipFile = false; try { - zipFile = new ZipFile(dexFile); - // if we get here, it's safe to assume we have a zip file - isZipFile = true; - - String zipEntryName = MoreObjects.firstNonNull(dexEntry, "classes.dex"); - ZipEntry zipEntry = zipFile.getEntry(zipEntryName); - if (zipEntry == null) { - throw new DexFileNotFound("zip file %s does not contain a %s file", dexFile.getName(), zipEntryName); - } - long fileLength = zipEntry.getSize(); - if (fileLength < 40) { - throw new ExceptionWithContext("The %s file in %s is too small to be a valid dex file", - zipEntryName, dexFile.getName()); - } else if (fileLength > Integer.MAX_VALUE) { - throw new ExceptionWithContext("The %s file in %s is too large to read in", - zipEntryName, dexFile.getName()); - } - byte[] dexBytes = new byte[(int)fileLength]; - ByteStreams.readFully(zipFile.getInputStream(zipEntry), dexBytes); - return new DexBackedDexFile(opcodes, dexBytes); - } catch (IOException ex) { - // don't continue on if we know it's a zip file - if (isZipFile) { - throw ex; - } - } finally { - if (zipFile != null) { - try { - zipFile.close(); - } catch (IOException ex) { - // just eat it - } - } + ZipDexContainer container = new ZipDexContainer(file, opcodes); + return new DexEntryFinder(file.getPath(), container).findEntry("classes.dex", true); + } catch (NotAZipFileException ex) { + // eat it and continue } - InputStream inputStream = new BufferedInputStream(new FileInputStream(dexFile)); + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); try { try { return DexBackedDexFile.fromInputStream(opcodes, inputStream); @@ -127,17 +99,18 @@ public final class DexFileFactory { // just eat it } - // Note: DexBackedDexFile.fromInputStream will reset inputStream back to the same position, if it fails - try { return DexBackedOdexFile.fromInputStream(opcodes, inputStream); } catch (DexBackedOdexFile.NotAnOdexFile ex) { // just eat it } + // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream + // back to the same position, if they fails + OatFile oatFile = null; try { - oatFile = OatFile.fromInputStream(inputStream); + oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file)); } catch (NotAnOatFileException ex) { // just eat it } @@ -150,77 +123,373 @@ public final class DexFileFactory { List<OatDexFile> oatDexFiles = oatFile.getDexFiles(); if (oatDexFiles.size() == 0) { - throw new DexFileNotFound("Oat file %s contains no dex files", dexFile.getName()); + throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName()); } - if (dexEntry == null) { - if (oatDexFiles.size() > 1) { - throw new MultipleDexFilesException(oatFile); - } - return oatDexFiles.get(0); - } else { - // first check for an exact match - for (OatDexFile oatDexFile : oatFile.getDexFiles()) { - if (oatDexFile.filename.equals(dexEntry)) { - return oatDexFile; - } - } + return oatDexFiles.get(0); + } + } finally { + inputStream.close(); + } - if (!dexEntry.contains("/")) { - for (OatDexFile oatDexFile : oatFile.getDexFiles()) { - File oatEntryFile = new File(oatDexFile.filename); - if (oatEntryFile.getName().equals(dexEntry)) { - return oatDexFile; - } - } - } + throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath()); + } + + /** + * Loads a dex entry from a container format (zip/oat) + * + * This has two modes of operation, depending on the exactMatch parameter. When exactMatch is true, it will only + * load an entry whose name exactly matches that provided by the dexEntry parameter. + * + * When exactMatch is false, then it will search for any entry that dexEntry is a path suffix of. "path suffix" + * meaning all the path components in dexEntry must fully match the corresponding path components in the entry name, + * but some path components at the beginning of entry name can be missing. + * + * For example, if an oat file contains a "/system/framework/framework.jar:classes2.dex" entry, then the following + * will match (not an exhaustive list): + * + * "/system/framework/framework.jar:classes2.dex" + * "system/framework/framework.jar:classes2.dex" + * "framework/framework.jar:classes2.dex" + * "framework.jar:classes2.dex" + * "classes2.dex" + * + * Note that partial path components specifically don't match. So something like "work/framework.jar:classes2.dex" + * would not match. + * + * If dexEntry contains an initial slash, it will be ignored for purposes of this suffix match -- but not when + * performing an exact match. + * + * If multiple entries match the given dexEntry, a MultipleMatchingDexEntriesException will be thrown + * + * @param file The container file. This must be either a zip (apk) file or an oat file. + * @param dexEntry The name of the entry to load. This can either be the exact entry name, if exactMatch is true, + * or it can be a path suffix. + * @param exactMatch If true, dexE + * @param opcodes The set of opcodes to use + * @return A DexBackedDexFile for the given entry + * + * @throws UnsupportedOatVersionException If file refers to an unsupported oat file + * @throws DexFileNotFoundException If the file does not exist, or if no matching entry could be found + * @throws UnsupportedFileTypeException If file is not a valid zip/oat file, or if the matching entry is not a + * valid dex file + * @throws MultipleMatchingDexEntriesException If multiple entries match the given dexEntry + */ + public static DexBackedDexFile loadDexEntry(@Nonnull File file, @Nonnull String dexEntry, + boolean exactMatch, @Nonnull Opcodes opcodes) throws IOException { + if (!file.exists()) { + throw new DexFileNotFoundException("Container file %s does not exist", file.getName()); + } + + try { + ZipDexContainer container = new ZipDexContainer(file, opcodes); + return new DexEntryFinder(file.getPath(), container).findEntry(dexEntry, exactMatch); + } catch (NotAZipFileException ex) { + // eat it and continue + } + + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + try { + OatFile oatFile = null; + try { + oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file)); + } catch (NotAnOatFileException ex) { + // just eat it + } - throw new DexFileNotFound("oat file %s does not contain a dex file named %s", - dexFile.getName(), dexEntry); + if (oatFile != null) { + if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { + throw new UnsupportedOatVersionException(oatFile); + } + + List<OatDexFile> oatDexFiles = oatFile.getDexFiles(); + + if (oatDexFiles.size() == 0) { + throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName()); } + + return new DexEntryFinder(file.getPath(), oatFile).findEntry(dexEntry, exactMatch); } } finally { inputStream.close(); } - throw new ExceptionWithContext("%s is not an apk, dex, odex or oat file.", dexFile.getPath()); + throw new UnsupportedFileTypeException("%s is not an apk or oat file.", file.getPath()); } + /** + * Loads a file containing 1 or more dex files + * + * If the given file is a dex or odex file, it will return a MultiDexContainer containing that single entry. + * Otherwise, for an oat or zip file, it will return an OatFile or ZipDexContainer respectively. + * + * @param file The file to open + * @param opcodes The set of opcodes to use + * @return A MultiDexContainer + * @throws DexFileNotFoundException If the given file does not exist + * @throws UnsupportedFileTypeException If the given file is not a valid dex/zip/odex/oat file + */ + public static MultiDexContainer<? extends DexBackedDexFile> loadDexContainer( + @Nonnull File file, @Nonnull final Opcodes opcodes) throws IOException { + if (!file.exists()) { + throw new DexFileNotFoundException("%s does not exist", file.getName()); + } + + ZipDexContainer zipDexContainer = new ZipDexContainer(file, opcodes); + if (zipDexContainer.isZipFile()) { + return zipDexContainer; + } + + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + try { + try { + DexBackedDexFile dexFile = DexBackedDexFile.fromInputStream(opcodes, inputStream); + return new SingletonMultiDexContainer(file.getPath(), dexFile); + } catch (DexBackedDexFile.NotADexFile ex) { + // just eat it + } + + try { + DexBackedOdexFile odexFile = DexBackedOdexFile.fromInputStream(opcodes, inputStream); + return new SingletonMultiDexContainer(file.getPath(), odexFile); + } catch (DexBackedOdexFile.NotAnOdexFile ex) { + // just eat it + } + + // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream + // back to the same position, if they fails + + OatFile oatFile = null; + try { + oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file)); + } catch (NotAnOatFileException ex) { + // just eat it + } + + if (oatFile != null) { + // TODO: we should support loading earlier oat files, just not deodexing them + if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { + throw new UnsupportedOatVersionException(oatFile); + } + return oatFile; + } + } finally { + inputStream.close(); + } + + throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath()); + } + + /** + * Writes a DexFile out to disk + * + * @param path The path to write the dex file to + * @param dexFile a DexFile to write + */ public static void writeDexFile(@Nonnull String path, @Nonnull DexFile dexFile) throws IOException { DexPool.writeTo(path, dexFile); } private DexFileFactory() {} - public static class DexFileNotFound extends ExceptionWithContext { - public DexFileNotFound(@Nullable Throwable cause) { - super(cause); + public static class DexFileNotFoundException extends ExceptionWithContext { + public DexFileNotFoundException(@Nullable String message, Object... formatArgs) { + super(message, formatArgs); } + } + + public static class UnsupportedOatVersionException extends ExceptionWithContext { + @Nonnull public final OatFile oatFile; - public DexFileNotFound(@Nullable Throwable cause, @Nullable String message, Object... formatArgs) { - super(cause, message, formatArgs); + public UnsupportedOatVersionException(@Nonnull OatFile oatFile) { + super("Unsupported oat version: %d", oatFile.getOatVersion()); + this.oatFile = oatFile; } + } - public DexFileNotFound(@Nullable String message, Object... formatArgs) { - super(message, formatArgs); + public static class MultipleMatchingDexEntriesException extends ExceptionWithContext { + public MultipleMatchingDexEntriesException(@Nonnull String message, Object... formatArgs) { + super(String.format(message, formatArgs)); } } - public static class MultipleDexFilesException extends ExceptionWithContext { - @Nonnull public final OatFile oatFile; + public static class UnsupportedFileTypeException extends ExceptionWithContext { + public UnsupportedFileTypeException(@Nonnull String message, Object... formatArgs) { + super(String.format(message, formatArgs)); + } + } - public MultipleDexFilesException(@Nonnull OatFile oatFile) { - super("Oat file has multiple dex files."); - this.oatFile = oatFile; + /** + * Matches two entries fully, ignoring any initial slash, if any + */ + private static boolean fullEntryMatch(@Nonnull String entry, @Nonnull String targetEntry) { + if (entry.equals(targetEntry)) { + return true; } + + if (entry.charAt(0) == '/') { + entry = entry.substring(1); + } + + if (targetEntry.charAt(0) == '/') { + targetEntry = targetEntry.substring(1); + } + + return entry.equals(targetEntry); } - public static class UnsupportedOatVersionException extends ExceptionWithContext { - @Nonnull public final OatFile oatFile; + /** + * Performs a partial match against entry and targetEntry. + * + * This is considered a partial match if targetEntry is a suffix of entry, and if the suffix starts + * on a path "part" (ignoring the initial separator, if any). Both '/' and ':' are considered separators for this. + * + * So entry="/blah/blah/something.dex" and targetEntry="lah/something.dex" shouldn't match, but + * both targetEntry="blah/something.dex" and "/blah/something.dex" should match. + */ + private static boolean partialEntryMatch(String entry, String targetEntry) { + if (entry.equals(targetEntry)) { + return true; + } - public UnsupportedOatVersionException(@Nonnull OatFile oatFile) { - super("Unsupported oat version: %d", oatFile.getOatVersion()); - this.oatFile = oatFile; + if (!entry.endsWith(targetEntry)) { + return false; + } + + // Make sure the first matching part is a full entry. We don't want to match "/blah/blah/something.dex" with + // "lah/something.dex", but both "/blah/something.dex" and "blah/something.dex" should match + char precedingChar = entry.charAt(entry.length() - targetEntry.length() - 1); + char firstTargetChar = targetEntry.charAt(0); + // This is a device path, so we should always use the linux separator '/', rather than the current platform's + // separator + return firstTargetChar == ':' || firstTargetChar == '/' || precedingChar == ':' || precedingChar == '/'; + } + + protected static class DexEntryFinder { + private final String filename; + private final MultiDexContainer<? extends DexBackedDexFile> dexContainer; + + public DexEntryFinder(@Nonnull String filename, + @Nonnull MultiDexContainer<? extends DexBackedDexFile> dexContainer) { + this.filename = filename; + this.dexContainer = dexContainer; + } + + @Nonnull + public DexBackedDexFile findEntry(@Nonnull String targetEntry, boolean exactMatch) throws IOException { + if (exactMatch) { + try { + DexBackedDexFile dexFile = dexContainer.getEntry(targetEntry); + if (dexFile == null) { + throw new DexFileNotFoundException("Could not find entry %s in %s.", targetEntry, filename); + } + return dexFile; + } catch (NotADexFile ex) { + throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", targetEntry, filename); + } + } + + // find all full and partial matches + List<String> fullMatches = Lists.newArrayList(); + List<DexBackedDexFile> fullEntries = Lists.newArrayList(); + List<String> partialMatches = Lists.newArrayList(); + List<DexBackedDexFile> partialEntries = Lists.newArrayList(); + for (String entry: dexContainer.getDexEntryNames()) { + if (fullEntryMatch(entry, targetEntry)) { + // We want to grab all full matches, regardless of whether they're actually a dex file. + fullMatches.add(entry); + fullEntries.add(dexContainer.getEntry(entry)); + } else if (partialEntryMatch(entry, targetEntry)) { + partialMatches.add(entry); + partialEntries.add(dexContainer.getEntry(entry)); + } + } + + // full matches always take priority + if (fullEntries.size() == 1) { + try { + DexBackedDexFile dexFile = fullEntries.get(0); + assert dexFile != null; + return dexFile; + } catch (NotADexFile ex) { + throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", + fullMatches.get(0), filename); + } + } + if (fullEntries.size() > 1) { + // This should be quite rare. This would only happen if an oat file has two entries that differ + // only by an initial path separator. e.g. "/blah/blah.dex" and "blah/blah.dex" + throw new MultipleMatchingDexEntriesException(String.format( + "Multiple entries in %s match %s: %s", filename, targetEntry, + Joiner.on(", ").join(fullMatches))); + } + + if (partialEntries.size() == 0) { + throw new DexFileNotFoundException("Could not find a dex entry in %s matching %s", + filename, targetEntry); + } + if (partialEntries.size() > 1) { + throw new MultipleMatchingDexEntriesException(String.format( + "Multiple dex entries in %s match %s: %s", filename, targetEntry, + Joiner.on(", ").join(partialMatches))); + } + return partialEntries.get(0); + } + } + + private static class SingletonMultiDexContainer implements MultiDexContainer<DexBackedDexFile> { + private final String entryName; + private final DexBackedDexFile dexFile; + + public SingletonMultiDexContainer(@Nonnull String entryName, @Nonnull DexBackedDexFile dexFile) { + this.entryName = entryName; + this.dexFile = dexFile; + } + + @Nonnull @Override public List<String> getDexEntryNames() throws IOException { + return ImmutableList.of(entryName); + } + + @Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException { + if (entryName.equals(this.entryName)) { + return dexFile; + } + return null; + } + + @Nonnull @Override public Opcodes getOpcodes() { + return dexFile.getOpcodes(); + } + } + + public static class FilenameVdexProvider implements VdexProvider { + private final File vdexFile; + + @Nullable + private byte[] buf = null; + private boolean loadedVdex = false; + + public FilenameVdexProvider(File oatFile) { + File oatParent = oatFile.getAbsoluteFile().getParentFile(); + String baseName = Files.getNameWithoutExtension(oatFile.getAbsolutePath()); + vdexFile = new File(oatParent, baseName + ".vdex"); + } + + @Nullable @Override public byte[] getVdex() { + if (!loadedVdex) { + if (vdexFile.exists()) { + try { + buf = ByteStreams.toByteArray(new FileInputStream(vdexFile)); + } catch (FileNotFoundException e) { + buf = null; + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + loadedVdex = true; + } + + return buf; } } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java b/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java index 843550f8..60dffa2f 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java @@ -330,8 +330,6 @@ public enum Opcode public static final int JUMBO_OPCODE = 0x200; //if the instruction can initialize an uninitialized object reference public static final int CAN_INITIALIZE_REFERENCE = 0x400; - //if the instruction is experimental (not potentially supported by Android runtime yet) - public static final int EXPERIMENTAL = 0x800; private static final int ALL_APIS = 0xFFFF0000; @@ -471,10 +469,6 @@ public enum Opcode return (flags & CAN_INITIALIZE_REFERENCE) != 0; } - public final boolean isExperimental() { - return (flags & EXPERIMENTAL) != 0; - } - private static class VersionConstraint { @Nonnull public final Range<Integer> apiRange; @Nonnull public final Range<Integer> artVersionRange; diff --git a/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java b/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java index a137dee2..5f8106d2 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java @@ -56,41 +56,32 @@ public class Opcodes { @Nonnull public static Opcodes forApi(int api) { - return new Opcodes(api, NO_VERSION, false); - } - - @Nonnull - public static Opcodes forApi(int api, boolean experimental) { - return new Opcodes(api, NO_VERSION, experimental); + return new Opcodes(api, NO_VERSION); } @Nonnull public static Opcodes forArtVersion(int artVersion) { - return forArtVersion(artVersion, false); + return new Opcodes(NO_VERSION, artVersion); } + /** + * @return a default Opcodes instance for when the exact Opcodes to use doesn't matter or isn't known + */ @Nonnull - public static Opcodes forArtVersion(int artVersion, boolean experimental) { - return new Opcodes(NO_VERSION, artVersion, experimental); + public static Opcodes getDefault() { + // The last pre-art api + return forApi(20); } - @Deprecated - public Opcodes(int api) { - this(api, false); - } + private Opcodes(int api, int artVersion) { - @Deprecated - public Opcodes(int api, boolean experimental) { - this(api, VersionMap.mapApiToArtVersion(api), experimental); - } - private Opcodes(int api, int artVersion, boolean experimental) { if (api >= 21) { - this.api = api; + this.api = api; this.artVersion = mapApiToArtVersion(api); } else if (artVersion >= 0 && artVersion < 39) { this.api = mapArtVersionToApi(artVersion); - this.artVersion = artVersion; + this.artVersion = artVersion; } else { this.api = api; this.artVersion = artVersion; @@ -116,7 +107,7 @@ public class Opcodes { } Short opcodeValue = versionToValueMap.get(version); - if (opcodeValue != null && (!opcode.isExperimental() || experimental)) { + if (opcodeValue != null) { if (!opcode.format.isPayloadFormat) { opcodesByValue[opcodeValue] = opcode; } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java index 55f1ddc7..1a9b9ad1 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java @@ -32,12 +32,14 @@ package org.jf.dexlib2.analysis; import com.google.common.base.Objects; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.jf.dexlib2.Opcode; import org.jf.dexlib2.iface.instruction.*; import org.jf.dexlib2.iface.instruction.formats.Instruction22c; import org.jf.dexlib2.iface.reference.MethodReference; import org.jf.dexlib2.iface.reference.Reference; +import org.jf.dexlib2.iface.reference.TypeReference; import org.jf.util.ExceptionWithContext; import javax.annotation.Nonnull; @@ -54,7 +56,7 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { /** * The actual instruction */ - @Nullable + @Nonnull protected Instruction instruction; /** @@ -65,21 +67,25 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { /** * Instructions that can pass on execution to this one during normal execution */ + @Nonnull protected final TreeSet<AnalyzedInstruction> predecessors = new TreeSet<AnalyzedInstruction>(); /** * Instructions that can execution could pass on to next during normal execution */ + @Nonnull protected final LinkedList<AnalyzedInstruction> successors = new LinkedList<AnalyzedInstruction>(); /** * This contains the register types *before* the instruction has executed */ + @Nonnull protected final RegisterType[] preRegisterMap; /** * This contains the register types *after* the instruction has executed */ + @Nonnull protected final RegisterType[] postRegisterMap; /** @@ -94,8 +100,8 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { */ protected final Instruction originalInstruction; - public AnalyzedInstruction(MethodAnalyzer methodAnalyzer, Instruction instruction, int instructionIndex, - int registerCount) { + public AnalyzedInstruction(@Nonnull MethodAnalyzer methodAnalyzer, @Nonnull Instruction instruction, + int instructionIndex, int registerCount) { this.methodAnalyzer = methodAnalyzer; this.instruction = instruction; this.originalInstruction = instruction; @@ -150,18 +156,17 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { instruction = originalInstruction; } - public int getSuccessorCount() { - return successors.size(); - } - - public List<AnalyzedInstruction> getSuccesors() { + @Nonnull + public List<AnalyzedInstruction> getSuccessors() { return Collections.unmodifiableList(successors); } + @Nonnull public Instruction getInstruction() { return instruction; } + @Nonnull public Instruction getOriginalInstruction() { return originalInstruction; } @@ -184,11 +189,7 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { if (predecessors.size() == 0) { return false; } - - if (predecessors.first().instructionIndex == -1) { - return true; - } - return false; + return predecessors.first().instructionIndex == -1; } /* @@ -237,6 +238,7 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { * @param registerNumber the register number * @return The register type resulting from merging the post-instruction register types from all predecessors */ + @Nonnull protected RegisterType getMergedPreRegisterTypeFromPredecessors(int registerNumber) { RegisterType mergedRegisterType = null; for (AnalyzedInstruction predecessor: predecessors) { @@ -249,6 +251,10 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { } } } + if (mergedRegisterType == null) { + // This is a start-of-method or unreachable instruction. + throw new IllegalStateException(); + } return mergedRegisterType; } /** @@ -275,10 +281,10 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { * * This is used to set the register type for only one branch from a conditional jump. * - * @param predecessor Which predecessor is being overriden - * @param registerNumber The register number of the register being overriden + * @param predecessor Which predecessor is being overridden + * @param registerNumber The register number of the register being overridden * @param registerType The overridden register type - * @param verifiedInstructions + * @param verifiedInstructions A bit vector of instructions that have been verified * * @return true if the post-instruction register type for this instruction changed as a result of this override */ @@ -308,8 +314,8 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { return false; } - protected boolean isInvokeInit() { - if (instruction == null || !instruction.getOpcode().canInitializeReference()) { + public boolean isInvokeInit() { + if (!instruction.getOpcode().canInitializeReference()) { return false; } @@ -323,23 +329,26 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { return false; } - public boolean setsRegister() { - return instruction.getOpcode().setsRegister(); - } - - public boolean setsWideRegister() { - return instruction.getOpcode().setsWideRegister(); - } - + /** + * Determines if this instruction sets the given register, or alters its type + * + * @param registerNumber The register to check + * @return true if this instruction sets the given register or alters its type + */ public boolean setsRegister(int registerNumber) { - //When constructing a new object, the register type will be an uninitialized reference after the new-instance - //instruction, but becomes an initialized reference once the <init> method is called. So even though invoke - //instructions don't normally change any registers, calling an <init> method will change the type of its - //object register. If the uninitialized reference has been copied to other registers, they will be initialized - //as well, so we need to check for that too + // This method could be implemented by calling getSetRegisters and checking if registerNumber is in the result + // However, this is a frequently called method, and this is a more efficient implementation, because it doesn't + // allocate a new list, and it can potentially exit earlier + if (isInvokeInit()) { + // When constructing a new object, the register type will be an uninitialized reference after the + // new-instance instruction, but becomes an initialized reference once the <init> method is called. So even + // though invoke instructions don't normally change any registers, calling an <init> method will change the + // type of its object register. If the uninitialized reference has been copied to other registers, they will + // be initialized as well, so we need to check for that too int destinationRegister; if (instruction instanceof FiveRegisterInstruction) { + assert ((FiveRegisterInstruction)instruction).getRegisterCount() > 0; destinationRegister = ((FiveRegisterInstruction)instruction).getRegisterC(); } else { assert instruction instanceof RegisterRangeInstruction; @@ -348,34 +357,107 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { destinationRegister = rangeInstruction.getStartRegister(); } - if (registerNumber == destinationRegister) { - return true; + RegisterType preInstructionDestRegisterType = getPreInstructionRegisterType(destinationRegister); + if (preInstructionDestRegisterType.category == RegisterType.UNKNOWN) { + // We never let an uninitialized reference propagate past an invoke-init if the object register type is + // unknown This is because the uninitialized reference may be an alias to the reference being + // initialized, but we can't know that until the object register's type is known + RegisterType preInstructionRegisterType = getPreInstructionRegisterType(registerNumber); + if (preInstructionRegisterType.category == RegisterType.UNINIT_REF || + preInstructionRegisterType.category == RegisterType.UNINIT_THIS) { + return true; + } } - RegisterType preInstructionDestRegisterType = getPreInstructionRegisterType(registerNumber); - if (preInstructionDestRegisterType.category != RegisterType.UNINIT_REF && - preInstructionDestRegisterType.category != RegisterType.UNINIT_THIS) { + if (preInstructionDestRegisterType.category != RegisterType.UNINIT_REF && + preInstructionDestRegisterType.category != RegisterType.UNINIT_THIS) { return false; } - //check if the uninit ref has been copied to another register - if (getPreInstructionRegisterType(registerNumber).equals(preInstructionDestRegisterType)) { + + if (registerNumber == destinationRegister) { return true; } - return false; + + //check if the uninit ref has been copied to another register + return preInstructionDestRegisterType.equals(getPreInstructionRegisterType(registerNumber)); } - if (instruction.getOpcode() == Opcode.IF_EQZ || instruction.getOpcode() == Opcode.IF_NEZ) { - AnalyzedInstruction previousInstruction = getPreviousInstruction(); - if (previousInstruction != null && - previousInstruction.instruction != null && - previousInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF && - registerNumber == ((Instruction22c)previousInstruction.instruction).getRegisterB() && - MethodAnalyzer.canNarrowAfterInstanceOf(previousInstruction, this, methodAnalyzer.getClassPath())) { - return true; + // On art, the optimizer will often nop out a check-cast instruction after an instance-of instruction. + // Normally, check-cast is where the register type actually changes. + // In order to correctly handle this case, we have to propagate the narrowed register type to the appropriate + // branch of the following if-eqz/if-nez + if (instructionIndex > 0 && + methodAnalyzer.getClassPath().isArt() && + getPredecessorCount() == 1 && + (instruction.getOpcode() == Opcode.IF_EQZ || instruction.getOpcode() == Opcode.IF_NEZ)) { + + AnalyzedInstruction prevInstruction = predecessors.first(); + if (prevInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF && + MethodAnalyzer.canPropagateTypeAfterInstanceOf( + prevInstruction, this, methodAnalyzer.getClassPath())) { + Instruction22c instanceOfInstruction = (Instruction22c)prevInstruction.instruction; + + if (registerNumber == instanceOfInstruction.getRegisterB()) { + return true; + } + + // Additionally, there may be a move instruction just before the instance-of, in order to put the value + // into a register that is addressable by the instance-of. In this case, we also need to propagate the + // new register type for the original register that the value was moved from. + // In some cases, the instance-of may have multiple predecessors. In this case, we should only do the + // propagation if all predecessors are move-object instructions for the same source register + // TODO: do we need to do some sort of additional check that these multiple move-object predecessors actually refer to the same value? + if (instructionIndex > 1) { + int originalSourceRegister = -1; + + RegisterType newType = null; + + for (AnalyzedInstruction prevPrevAnalyzedInstruction : prevInstruction.predecessors) { + Opcode opcode = prevPrevAnalyzedInstruction.instruction.getOpcode(); + if (opcode == Opcode.MOVE_OBJECT || opcode == Opcode.MOVE_OBJECT_16 || + opcode == Opcode.MOVE_OBJECT_FROM16) { + TwoRegisterInstruction moveInstruction = + ((TwoRegisterInstruction)prevPrevAnalyzedInstruction.instruction); + RegisterType originalType = + prevPrevAnalyzedInstruction.getPostInstructionRegisterType( + moveInstruction.getRegisterB()); + if (moveInstruction.getRegisterA() != instanceOfInstruction.getRegisterB()) { + originalSourceRegister = -1; + break; + } + if (originalType.type == null) { + originalSourceRegister = -1; + break; + } + + if (newType == null) { + newType = RegisterType.getRegisterType(methodAnalyzer.getClassPath(), + (TypeReference)instanceOfInstruction.getReference()); + } + + if (MethodAnalyzer.isNotWideningConversion(originalType, newType)) { + if (originalSourceRegister != -1) { + if (originalSourceRegister != moveInstruction.getRegisterB()) { + originalSourceRegister = -1; + break; + } + } else { + originalSourceRegister = moveInstruction.getRegisterB(); + } + } + } else { + originalSourceRegister = -1; + break; + } + } + if (originalSourceRegister != -1 && registerNumber == originalSourceRegister) { + return true; + } + } } } - if (!setsRegister()) { + if (!instruction.getOpcode().setsRegister()) { return false; } int destinationRegister = getDestinationRegister(); @@ -383,20 +465,151 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { if (registerNumber == destinationRegister) { return true; } - if (setsWideRegister() && registerNumber == (destinationRegister + 1)) { + if (instruction.getOpcode().setsWideRegister() && registerNumber == (destinationRegister + 1)) { return true; } return false; } - @Nullable - private AnalyzedInstruction getPreviousInstruction() { - for (AnalyzedInstruction predecessor: predecessors) { - if (predecessor.getInstructionIndex() == getInstructionIndex() - 1) { - return predecessor; + public List<Integer> getSetRegisters() { + List<Integer> setRegisters = Lists.newArrayList(); + + if (instruction.getOpcode().setsRegister()) { + setRegisters.add(getDestinationRegister()); + } + if (instruction.getOpcode().setsWideRegister()) { + setRegisters.add(getDestinationRegister() + 1); + } + + if (isInvokeInit()) { + //When constructing a new object, the register type will be an uninitialized reference after the new-instance + //instruction, but becomes an initialized reference once the <init> method is called. So even though invoke + //instructions don't normally change any registers, calling an <init> method will change the type of its + //object register. If the uninitialized reference has been copied to other registers, they will be initialized + //as well, so we need to check for that too + + int destinationRegister; + if (instruction instanceof FiveRegisterInstruction) { + destinationRegister = ((FiveRegisterInstruction)instruction).getRegisterC(); + assert ((FiveRegisterInstruction)instruction).getRegisterCount() > 0; + } else { + assert instruction instanceof RegisterRangeInstruction; + RegisterRangeInstruction rangeInstruction = (RegisterRangeInstruction)instruction; + assert rangeInstruction.getRegisterCount() > 0; + destinationRegister = rangeInstruction.getStartRegister(); + } + + RegisterType preInstructionDestRegisterType = getPreInstructionRegisterType(destinationRegister); + if (preInstructionDestRegisterType.category == RegisterType.UNINIT_REF || + preInstructionDestRegisterType.category == RegisterType.UNINIT_THIS) { + setRegisters.add(destinationRegister); + + RegisterType objectRegisterType = preRegisterMap[destinationRegister]; + for (int i = 0; i < preRegisterMap.length; i++) { + if (i == destinationRegister) { + continue; + } + + RegisterType preInstructionRegisterType = preRegisterMap[i]; + + if (preInstructionRegisterType.equals(objectRegisterType)) { + setRegisters.add(i); + } else if (preInstructionRegisterType.category == RegisterType.UNINIT_REF || + preInstructionRegisterType.category == RegisterType.UNINIT_THIS) { + RegisterType postInstructionRegisterType = postRegisterMap[i]; + if (postInstructionRegisterType.category == RegisterType.UNKNOWN) { + setRegisters.add(i); + } + } + } + } else if (preInstructionDestRegisterType.category == RegisterType.UNKNOWN) { + // We never let an uninitialized reference propagate past an invoke-init if the object register type is + // unknown This is because the uninitialized reference may be an alias to the reference being + // initialized, but we can't know that until the object register's type is known + + for (int i = 0; i < preRegisterMap.length; i++) { + RegisterType registerType = preRegisterMap[i]; + if (registerType.category == RegisterType.UNINIT_REF || + registerType.category == RegisterType.UNINIT_THIS) { + setRegisters.add(i); + } + } + } + } + + // On art, the optimizer will often nop out a check-cast instruction after an instance-of instruction. + // Normally, check-cast is where the register type actually changes. + // In order to correctly handle this case, we have to propagate the narrowed register type to the appropriate + // branch of the following if-eqz/if-nez + if (instructionIndex > 0 && + methodAnalyzer.getClassPath().isArt() && + getPredecessorCount() == 1 && + (instruction.getOpcode() == Opcode.IF_EQZ || instruction.getOpcode() == Opcode.IF_NEZ)) { + + AnalyzedInstruction prevInstruction = predecessors.first(); + if (prevInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF && + MethodAnalyzer.canPropagateTypeAfterInstanceOf( + prevInstruction, this, methodAnalyzer.getClassPath())) { + Instruction22c instanceOfInstruction = (Instruction22c)prevInstruction.instruction; + setRegisters.add(instanceOfInstruction.getRegisterB()); + + // Additionally, there may be a move instruction just before the instance-of, in order to put the value + // into a register that is addressable by the instance-of. In this case, we also need to propagate the + // new register type for the original register that the value was moved from. + // In some cases, the instance-of may have multiple predecessors. In this case, we should only do the + // propagation if all predecessors are move-object instructions for the same source register + // TODO: do we need to do some sort of additional check that these multiple move-object predecessors actually refer to the same value? + if (instructionIndex > 1) { + int originalSourceRegister = -1; + + RegisterType newType = null; + + for (AnalyzedInstruction prevPrevAnalyzedInstruction : prevInstruction.predecessors) { + Opcode opcode = prevPrevAnalyzedInstruction.instruction.getOpcode(); + if (opcode == Opcode.MOVE_OBJECT || opcode == Opcode.MOVE_OBJECT_16 || + opcode == Opcode.MOVE_OBJECT_FROM16) { + TwoRegisterInstruction moveInstruction = + ((TwoRegisterInstruction)prevPrevAnalyzedInstruction.instruction); + RegisterType originalType = + prevPrevAnalyzedInstruction.getPostInstructionRegisterType( + moveInstruction.getRegisterB()); + if (moveInstruction.getRegisterA() != instanceOfInstruction.getRegisterB()) { + originalSourceRegister = -1; + break; + } + if (originalType.type == null) { + originalSourceRegister = -1; + break; + } + + if (newType == null) { + newType = RegisterType.getRegisterType(methodAnalyzer.getClassPath(), + (TypeReference)instanceOfInstruction.getReference()); + } + + if (MethodAnalyzer.isNotWideningConversion(originalType, newType)) { + if (originalSourceRegister != -1) { + if (originalSourceRegister != moveInstruction.getRegisterB()) { + originalSourceRegister = -1; + break; + } + } else { + originalSourceRegister = moveInstruction.getRegisterB(); + } + } + } else { + originalSourceRegister = -1; + break; + } + } + if (originalSourceRegister != -1) { + setRegisters.add(originalSourceRegister); + } + } } } - return null; + + return setRegisters; } public int getDestinationRegister() { @@ -421,7 +634,7 @@ public class AnalyzedInstruction implements Comparable<AnalyzedInstruction> { return preRegisterMap[registerNumber]; } - public int compareTo(AnalyzedInstruction analyzedInstruction) { + public int compareTo(@Nonnull AnalyzedInstruction analyzedInstruction) { if (instructionIndex < analyzedInstruction.instructionIndex) { return -1; } else if (instructionIndex == analyzedInstruction.instructionIndex) { diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java index 9f9e396b..48bf6181 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java @@ -36,28 +36,18 @@ import com.google.common.base.Suppliers; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; -import org.jf.dexlib2.DexFileFactory; -import org.jf.dexlib2.DexFileFactory.DexFileNotFound; -import org.jf.dexlib2.DexFileFactory.MultipleDexFilesException; import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.analysis.reflection.ReflectionClassDef; -import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; import org.jf.dexlib2.iface.ClassDef; -import org.jf.dexlib2.iface.DexFile; import org.jf.dexlib2.immutable.ImmutableDexFile; -import org.jf.util.ExceptionWithContext; import javax.annotation.Nonnull; -import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.Arrays; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class ClassPath { @Nonnull private final TypeProto unknownClass; @@ -70,8 +60,8 @@ public class ClassPath { /** * Creates a new ClassPath instance that can load classes from the given providers * - * @param classProviders An iterable of ClassProviders. When loading a class, these providers will be searched in - * order + * @param classProviders A varargs array of ClassProviders. When loading a class, these providers will be searched + * in order */ public ClassPath(ClassProvider... classProviders) throws IOException { this(Arrays.asList(classProviders), false, NOT_ART); @@ -82,6 +72,16 @@ public class ClassPath { * * @param classProviders An iterable of ClassProviders. When loading a class, these providers will be searched in * order + */ + public ClassPath(Iterable<ClassProvider> classProviders) throws IOException { + this(classProviders, false, NOT_ART); + } + + /** + * Creates a new ClassPath instance that can load classes from the given providers + * + * @param classProviders An iterable of ClassProviders. When loading a class, these providers will be searched in + * order * @param checkPackagePrivateAccess Whether checkPackagePrivateAccess is needed, enabled for ONLY early API 17 by * default * @param oatVersion The applicable oat version, or NOT_ART @@ -114,7 +114,7 @@ public class ClassPath { private static ClassProvider getBasicClasses() { // fallbacks for some special classes that we assume are present - return new DexClassProvider(new ImmutableDexFile(Opcodes.forApi(19), ImmutableSet.of( + return new DexClassProvider(new ImmutableDexFile(Opcodes.getDefault(), ImmutableSet.of( new ReflectionClassDef(Class.class), new ReflectionClassDef(Cloneable.class), new ReflectionClassDef(Object.class), @@ -164,119 +164,6 @@ public class ClassPath { return checkPackagePrivateAccess; } - @Nonnull - public static ClassPath fromClassPath(Iterable<String> classPathDirs, Iterable<String> classPath, DexFile dexFile, - int api, boolean experimental) { - return fromClassPath(classPathDirs, classPath, dexFile, api, api == 17, experimental); - } - - @Nonnull - public static ClassPath fromClassPath(Iterable<String> classPathDirs, Iterable<String> classPath, DexFile dexFile, - int api, boolean checkPackagePrivateAccess, boolean experimental) { - List<ClassProvider> providers = Lists.newArrayList(); - - int oatVersion = NOT_ART; - - for (String classPathEntry: classPath) { - List<? extends DexFile> classPathDexFiles = - loadClassPathEntry(classPathDirs, classPathEntry, api, experimental); - if (oatVersion == NOT_ART) { - for (DexFile classPathDexFile: classPathDexFiles) { - if (classPathDexFile instanceof OatDexFile) { - oatVersion = ((OatDexFile)classPathDexFile).getOatVersion(); - break; - } - } - } - for (DexFile classPathDexFile: classPathDexFiles) { - providers.add(new DexClassProvider(classPathDexFile)); - } - } - providers.add(new DexClassProvider(dexFile)); - return new ClassPath(providers, checkPackagePrivateAccess, oatVersion); - } - - @Nonnull - public static ClassPath fromClassPath(Iterable<String> classPathDirs, Iterable<String> classPath, DexFile dexFile, - int api, boolean checkPackagePrivateAccess, boolean experimental, - int oatVersion) { - List<ClassProvider> providers = Lists.newArrayList(); - - for (String classPathEntry: classPath) { - List<? extends DexFile> classPathDexFiles = - loadClassPathEntry(classPathDirs, classPathEntry, api, experimental); - for (DexFile classPathDexFile: classPathDexFiles) { - providers.add(new DexClassProvider(classPathDexFile)); - } - } - providers.add(new DexClassProvider(dexFile)); - return new ClassPath(providers, checkPackagePrivateAccess, oatVersion); - } - - private static final Pattern dalvikCacheOdexPattern = Pattern.compile("@([^@]+)@classes.dex$"); - - @Nonnull - private static List<? extends DexFile> loadClassPathEntry(@Nonnull Iterable<String> classPathDirs, - @Nonnull String bootClassPathEntry, int api, - boolean experimental) { - File rawEntry = new File(bootClassPathEntry); - // strip off the path - we only care about the filename - String entryName = rawEntry.getName(); - - // if it's a dalvik-cache entry, grab the name of the jar/apk - if (entryName.endsWith("@classes.dex")) { - Matcher m = dalvikCacheOdexPattern.matcher(entryName); - - if (!m.find()) { - throw new ExceptionWithContext(String.format("Cannot parse dependency value %s", bootClassPathEntry)); - } - - entryName = m.group(1); - } - - int extIndex = entryName.lastIndexOf("."); - - String baseEntryName; - if (extIndex == -1) { - baseEntryName = entryName; - } else { - baseEntryName = entryName.substring(0, extIndex); - } - - for (String classPathDir: classPathDirs) { - String[] extensions; - - if (entryName.endsWith(".oat")) { - extensions = new String[] { ".oat" }; - } else { - extensions = new String[] { "", ".odex", ".jar", ".apk", ".zip" }; - } - - for (String ext: extensions) { - File file = new File(classPathDir, baseEntryName + ext); - - if (file.exists() && file.isFile()) { - if (!file.canRead()) { - System.err.println(String.format( - "warning: cannot open %s for reading. Will continue looking.", file.getPath())); - } else { - try { - return ImmutableList.of(DexFileFactory.loadDexFile(file, api, experimental)); - } catch (DexFileNotFound ex) { - // ignore and continue - } catch (MultipleDexFilesException ex) { - return ex.oatFile.getDexFiles(); - } catch (Exception ex) { - throw ExceptionWithContext.withContext(ex, - "Error while reading boot class path entry \"%s\"", bootClassPathEntry); - } - } - } - } - } - throw new ExceptionWithContext("Cannot locate boot class path file %s", bootClassPathEntry); - } - private final Supplier<OdexedFieldInstructionMapper> fieldInstructionMapperSupplier = Suppliers.memoize( new Supplier<OdexedFieldInstructionMapper>() { @Override public OdexedFieldInstructionMapper get() { diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java new file mode 100644 index 00000000..10daa566 --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java @@ -0,0 +1,465 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2.analysis; + +import com.beust.jcommander.internal.Sets; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; +import org.jf.dexlib2.DexFileFactory; +import org.jf.dexlib2.DexFileFactory.UnsupportedFileTypeException; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.dexlib2.dexbacked.DexBackedOdexFile; +import org.jf.dexlib2.dexbacked.OatFile; +import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; +import org.jf.dexlib2.iface.DexFile; +import org.jf.dexlib2.iface.MultiDexContainer; +import org.jf.dexlib2.iface.MultiDexContainer.MultiDexFile; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +public class ClassPathResolver { + private final Iterable<String> classPathDirs; + private final Opcodes opcodes; + + private final Set<File> loadedFiles = Sets.newHashSet(); + private final List<ClassProvider> classProviders = Lists.newArrayList(); + + /** + * Constructs a new ClassPathResolver using a specified list of bootclasspath entries + * + * @param bootClassPathDirs A list of directories to search for boot classpath entries. Can be empty if all boot + * classpath entries are specified as local paths + * @param bootClassPathEntries A list of boot classpath entries to load. These can either be local paths, or + * device paths (e.g. "/system/framework/framework.jar"). The entry will be interpreted + * first as a local path. If not found as a local path, it will be interpreted as a + * partial or absolute device path, and will be searched for in bootClassPathDirs + * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be + * local paths. Device paths are not supported. + * @param dexFile The dex file that the classpath will be used to analyze + * @throws IOException If any IOException occurs + * @throws ResolveException If any classpath entries cannot be loaded for some reason + * + * If null, a default bootclasspath is used, + * depending on the the file type of dexFile and the api level. If empty, no boot + * classpath entries will be loaded + */ + public ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nonnull List<String> bootClassPathEntries, + @Nonnull List<String> extraClassPathEntries, @Nonnull DexFile dexFile) + throws IOException { + this(bootClassPathDirs, bootClassPathEntries, extraClassPathEntries, dexFile, true); + } + + /** + * Constructs a new ClassPathResolver using a default list of bootclasspath entries + * + * @param bootClassPathDirs A list of directories to search for boot classpath entries + * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be + * local paths. Device paths are not supported. + * @param dexFile The dex file that the classpath will be used to analyze + * @throws IOException If any IOException occurs + * @throws ResolveException If any classpath entries cannot be loaded for some reason + * + * If null, a default bootclasspath is used, + * depending on the the file type of dexFile and the api level. If empty, no boot + * classpath entries will be loaded + */ + public ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nonnull List<String> extraClassPathEntries, + @Nonnull DexFile dexFile) + throws IOException { + this(bootClassPathDirs, null, extraClassPathEntries, dexFile, true); + } + + private ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nullable List<String> bootClassPathEntries, + @Nonnull List<String> extraClassPathEntries, @Nonnull DexFile dexFile, boolean unused) + throws IOException { + this.classPathDirs = bootClassPathDirs; + opcodes = dexFile.getOpcodes(); + + if (bootClassPathEntries == null) { + bootClassPathEntries = getDefaultBootClassPath(dexFile, opcodes.api); + } + + for (String entry : bootClassPathEntries) { + try { + loadLocalOrDeviceBootClassPathEntry(entry); + } catch (NoDexException ex) { + if (entry.endsWith(".jar")) { + String odexEntry = entry.substring(0, entry.length() - 4) + ".odex"; + try { + loadLocalOrDeviceBootClassPathEntry(odexEntry); + } catch (NoDexException ex2) { + throw new ResolveException("Neither %s nor %s contain a dex file", entry, odexEntry); + } catch (NotFoundException ex2) { + throw new ResolveException(ex); + } + } else { + throw new ResolveException(ex); + } + } catch (NotFoundException ex) { + if (entry.endsWith(".odex")) { + String jarEntry = entry.substring(0, entry.length() - 5) + ".jar"; + try { + loadLocalOrDeviceBootClassPathEntry(jarEntry); + } catch (NoDexException ex2) { + throw new ResolveException("Neither %s nor %s contain a dex file", entry, jarEntry); + } catch (NotFoundException ex2) { + throw new ResolveException(ex); + } + } else { + throw new ResolveException(ex); + } + } + } + + for (String entry: extraClassPathEntries) { + // extra classpath entries must be specified using a local path, so we don't need to do the search through + // bootClassPathDirs + try { + loadLocalClassPathEntry(entry); + } catch (NoDexException ex) { + throw new ResolveException(ex); + } + } + + if (dexFile instanceof MultiDexContainer.MultiDexFile) { + MultiDexContainer<? extends MultiDexFile> container = ((MultiDexFile)dexFile).getContainer(); + for (String entry: container.getDexEntryNames()) { + classProviders.add(new DexClassProvider(container.getEntry(entry))); + } + } else { + classProviders.add(new DexClassProvider(dexFile)); + } + } + + @Nonnull + public List<ClassProvider> getResolvedClassProviders() { + return classProviders; + } + + private boolean loadLocalClassPathEntry(@Nonnull String entry) throws NoDexException, IOException { + File entryFile = new File(entry); + if (entryFile.exists() && entryFile.isFile()) { + try { + loadEntry(entryFile, true); + return true; + } catch (UnsupportedFileTypeException ex) { + throw new ResolveException(ex, "Couldn't load classpath entry %s", entry); + } + } + return false; + } + + private void loadLocalOrDeviceBootClassPathEntry(@Nonnull String entry) + throws IOException, NoDexException, NotFoundException { + // first, see if the entry is a valid local path + if (loadLocalClassPathEntry(entry)) { + return; + } + + // It's not a local path, so let's try to resolve it as a device path, relative to one of the provided + // directories + List<String> pathComponents = splitDevicePath(entry); + Joiner pathJoiner = Joiner.on(File.pathSeparatorChar); + + for (String directory: classPathDirs) { + File directoryFile = new File(directory); + if (!directoryFile.exists()) { + continue; + } + + for (int i=0; i<pathComponents.size(); i++) { + String partialPath = pathJoiner.join(pathComponents.subList(i, pathComponents.size())); + File entryFile = new File(directoryFile, partialPath); + if (entryFile.exists() && entryFile.isFile()) { + loadEntry(entryFile, true); + return; + } + } + } + + throw new NotFoundException("Could not find classpath entry %s", entry); + } + + private void loadEntry(@Nonnull File entryFile, boolean loadOatDependencies) + throws IOException, NoDexException { + if (loadedFiles.contains(entryFile)) { + return; + } + + MultiDexContainer<? extends DexBackedDexFile> container; + try { + container = DexFileFactory.loadDexContainer(entryFile, opcodes); + } catch (UnsupportedFileTypeException ex) { + throw new ResolveException(ex); + } + + List<String> entryNames = container.getDexEntryNames(); + + if (entryNames.size() == 0) { + throw new NoDexException("%s contains no dex file", entryFile); + } + + loadedFiles.add(entryFile); + + for (String entryName: entryNames) { + classProviders.add(new DexClassProvider(container.getEntry(entryName))); + } + + if (loadOatDependencies && container instanceof OatFile) { + List<String> oatDependencies = ((OatFile)container).getBootClassPath(); + if (!oatDependencies.isEmpty()) { + try { + loadOatDependencies(entryFile.getParentFile(), oatDependencies); + } catch (NotFoundException ex) { + throw new ResolveException(ex, "Error while loading oat file %s", entryFile); + } catch (NoDexException ex) { + throw new ResolveException(ex, "Error while loading dependencies for oat file %s", entryFile); + } + } + } + } + + @Nonnull + private static List<String> splitDevicePath(@Nonnull String path) { + return Lists.newArrayList(Splitter.on('/').split(path)); + } + + private void loadOatDependencies(@Nonnull File directory, @Nonnull List<String> oatDependencies) + throws IOException, NoDexException, NotFoundException { + // We assume that all oat dependencies are located in the same directory as the oat file + for (String oatDependency: oatDependencies) { + String oatDependencyName = getFilenameForOatDependency(oatDependency); + File file = new File(directory, oatDependencyName); + if (!file.exists()) { + throw new NotFoundException("Cannot find dependency %s in %s", oatDependencyName, directory); + } + + loadEntry(file, false); + } + } + + @Nonnull + private String getFilenameForOatDependency(String oatDependency) { + int index = oatDependency.lastIndexOf('/'); + + String dependencyLeaf = oatDependency.substring(index+1); + if (dependencyLeaf.endsWith(".art")) { + return dependencyLeaf.substring(0, dependencyLeaf.length() - 4) + ".oat"; + } + return dependencyLeaf; + } + + private static class NotFoundException extends Exception { + public NotFoundException(String message, Object... formatArgs) { + super(String.format(message, formatArgs)); + } + } + + private static class NoDexException extends Exception { + public NoDexException(String message, Object... formatArgs) { + super(String.format(message, formatArgs)); + } + } + + /** + * An error that occurred while resolving the classpath + */ + public static class ResolveException extends RuntimeException { + public ResolveException (String message, Object... formatArgs) { + super(String.format(message, formatArgs)); + } + + public ResolveException (Throwable cause) { + super(cause); + } + + public ResolveException (Throwable cause, String message, Object... formatArgs) { + super(String.format(message, formatArgs), cause); + } + } + + /** + * Returns the default boot class path for the given dex file and api level. + */ + @Nonnull + private static List<String> getDefaultBootClassPath(@Nonnull DexFile dexFile, int apiLevel) { + if (dexFile instanceof OatFile.OatDexFile) { + List<String> bcp = ((OatDexFile)dexFile).getContainer().getBootClassPath(); + if (!bcp.isEmpty()) { + for (int i=0; i<bcp.size(); i++) { + String entry = bcp.get(i); + if (entry.endsWith(".art")) { + bcp.set(i, entry.substring(0, entry.length() - 4) + ".oat"); + } + } + return bcp; + } + return Lists.newArrayList("boot.oat"); + } + + if (dexFile instanceof DexBackedOdexFile) { + return ((DexBackedOdexFile)dexFile).getDependencies(); + } + + if (apiLevel <= 8) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar"); + } else if (apiLevel <= 11) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/core-junit.jar"); + } else if (apiLevel <= 13) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/apache-xml.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/core-junit.jar"); + } else if (apiLevel <= 15) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/apache-xml.jar", + "/system/framework/filterfw.jar"); + } else if (apiLevel <= 17) { + // this is correct as of api 17/4.2.2 + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/telephony-common.jar", + "/system/framework/mms-common.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/apache-xml.jar"); + } else if (apiLevel <= 18) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/telephony-common.jar", + "/system/framework/voip-common.jar", + "/system/framework/mms-common.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/apache-xml.jar"); + } else if (apiLevel <= 19) { + return Lists.newArrayList( + "/system/framework/core.jar", + "/system/framework/conscrypt.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/framework2.jar", + "/system/framework/telephony-common.jar", + "/system/framework/voip-common.jar", + "/system/framework/mms-common.jar", + "/system/framework/android.policy.jar", + "/system/framework/services.jar", + "/system/framework/apache-xml.jar", + "/system/framework/webviewchromium.jar"); + } else if (apiLevel <= 22) { + return Lists.newArrayList( + "/system/framework/core-libart.jar", + "/system/framework/conscrypt.jar", + "/system/framework/okhttp.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/telephony-common.jar", + "/system/framework/voip-common.jar", + "/system/framework/ims-common.jar", + "/system/framework/mms-common.jar", + "/system/framework/android.policy.jar", + "/system/framework/apache-xml.jar"); + } else if (apiLevel <= 23) { + return Lists.newArrayList( + "/system/framework/core-libart.jar", + "/system/framework/conscrypt.jar", + "/system/framework/okhttp.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/telephony-common.jar", + "/system/framework/voip-common.jar", + "/system/framework/ims-common.jar", + "/system/framework/apache-xml.jar", + "/system/framework/org.apache.http.legacy.boot.jar"); + } else /*if (apiLevel <= 24)*/ { + return Lists.newArrayList( + "/system/framework/core-oj.jar", + "/system/framework/core-libart.jar", + "/system/framework/conscrypt.jar", + "/system/framework/okhttp.jar", + "/system/framework/core-junit.jar", + "/system/framework/bouncycastle.jar", + "/system/framework/ext.jar", + "/system/framework/framework.jar", + "/system/framework/telephony-common.jar", + "/system/framework/voip-common.jar", + "/system/framework/ims-common.jar", + "/system/framework/apache-xml.jar", + "/system/framework/org.apache.http.legacy.boot.jar"); + } + } +} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java index cef683ce..44cc5e24 100644..100755 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java @@ -31,6 +31,7 @@ package org.jf.dexlib2.analysis; +import com.google.common.base.Joiner; import com.google.common.base.Predicates; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; @@ -38,12 +39,10 @@ import com.google.common.collect.*; import com.google.common.primitives.Ints; import org.jf.dexlib2.AccessFlags; import org.jf.dexlib2.analysis.util.TypeProtoUtils; -import org.jf.dexlib2.iface.ClassDef; -import org.jf.dexlib2.iface.Field; -import org.jf.dexlib2.iface.Method; +import org.jf.dexlib2.base.reference.BaseMethodReference; +import org.jf.dexlib2.iface.*; import org.jf.dexlib2.iface.reference.FieldReference; import org.jf.dexlib2.iface.reference.MethodReference; -import org.jf.dexlib2.immutable.ImmutableMethod; import org.jf.dexlib2.util.MethodUtil; import org.jf.util.AlignmentUtils; import org.jf.util.ExceptionWithContext; @@ -52,6 +51,7 @@ import org.jf.util.SparseArray; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.*; +import java.util.Map.Entry; /** * A class "prototype". This contains things like the interfaces, the superclass, the vtable and the instance fields @@ -122,11 +122,18 @@ public class ClassProto implements TypeProto { */ @Nonnull protected LinkedHashMap<String, ClassDef> getInterfaces() { - return interfacesSupplier.get(); + if (!classPath.isArt() || classPath.oatVersion < 72) { + return preDefaultMethodInterfaceSupplier.get(); + } else { + return postDefaultMethodInterfaceSupplier.get(); + } } + /** + * This calculates the interfaces in the order required for vtable generation for dalvik and pre-default method ART + */ @Nonnull - private final Supplier<LinkedHashMap<String, ClassDef>> interfacesSupplier = + private final Supplier<LinkedHashMap<String, ClassDef>> preDefaultMethodInterfaceSupplier = Suppliers.memoize(new Supplier<LinkedHashMap<String, ClassDef>>() { @Override public LinkedHashMap<String, ClassDef> get() { Set<String> unresolvedInterfaces = new HashSet<String>(0); @@ -148,7 +155,8 @@ public class ClassProto implements TypeProto { ClassProto interfaceProto = (ClassProto) classPath.getClass(interfaceType); for (String superInterface: interfaceProto.getInterfaces().keySet()) { if (!interfaces.containsKey(superInterface)) { - interfaces.put(superInterface, interfaceProto.getInterfaces().get(superInterface)); + interfaces.put(superInterface, + interfaceProto.getInterfaces().get(superInterface)); } } if (!interfaceProto.interfacesFullyResolved) { @@ -158,6 +166,7 @@ public class ClassProto implements TypeProto { } } } catch (UnresolvedClassException ex) { + interfaces.put(type, null); unresolvedInterfaces.add(type); interfacesFullyResolved = false; } @@ -196,6 +205,71 @@ public class ClassProto implements TypeProto { } }); + /** + * This calculates the interfaces in the order required for vtable generation for post-default method ART + */ + @Nonnull + private final Supplier<LinkedHashMap<String, ClassDef>> postDefaultMethodInterfaceSupplier = + Suppliers.memoize(new Supplier<LinkedHashMap<String, ClassDef>>() { + @Override public LinkedHashMap<String, ClassDef> get() { + Set<String> unresolvedInterfaces = new HashSet<String>(0); + LinkedHashMap<String, ClassDef> interfaces = Maps.newLinkedHashMap(); + + String superclass = getSuperclass(); + if (superclass != null) { + ClassProto superclassProto = (ClassProto) classPath.getClass(superclass); + for (String superclassInterface: superclassProto.getInterfaces().keySet()) { + interfaces.put(superclassInterface, null); + } + if (!superclassProto.interfacesFullyResolved) { + unresolvedInterfaces.addAll(superclassProto.getUnresolvedInterfaces()); + interfacesFullyResolved = false; + } + } + + try { + for (String interfaceType: getClassDef().getInterfaces()) { + if (!interfaces.containsKey(interfaceType)) { + ClassProto interfaceProto = (ClassProto)classPath.getClass(interfaceType); + try { + for (Entry<String, ClassDef> entry: interfaceProto.getInterfaces().entrySet()) { + if (!interfaces.containsKey(entry.getKey())) { + interfaces.put(entry.getKey(), entry.getValue()); + } + } + } catch (UnresolvedClassException ex) { + interfaces.put(interfaceType, null); + unresolvedInterfaces.add(interfaceType); + interfacesFullyResolved = false; + } + if (!interfaceProto.interfacesFullyResolved) { + unresolvedInterfaces.addAll(interfaceProto.getUnresolvedInterfaces()); + interfacesFullyResolved = false; + } + try { + ClassDef interfaceDef = classPath.getClassDef(interfaceType); + interfaces.put(interfaceType, interfaceDef); + } catch (UnresolvedClassException ex) { + interfaces.put(interfaceType, null); + unresolvedInterfaces.add(interfaceType); + interfacesFullyResolved = false; + } + } + } + } catch (UnresolvedClassException ex) { + interfaces.put(type, null); + unresolvedInterfaces.add(type); + interfacesFullyResolved = false; + } + + if (unresolvedInterfaces.size() > 0) { + ClassProto.this.unresolvedInterfaces = unresolvedInterfaces; + } + + return interfaces; + } + }); + @Nonnull protected Set<String> getUnresolvedInterfaces() { if (unresolvedInterfaces == null) { @@ -219,7 +293,7 @@ public class ClassProto implements TypeProto { if (!interfacesFullyResolved) { throw new UnresolvedClassException("Interfaces for class %s not fully resolved: %s", getType(), - getUnresolvedInterfaces()); + Joiner.on(',').join(getUnresolvedInterfaces())); } return directInterfaces; @@ -378,7 +452,10 @@ public class ClassProto implements TypeProto { } public int findMethodIndexInVtable(@Nonnull MethodReference method) { - List<Method> vtable = getVtable(); + return findMethodIndexInVtable(getVtable(), method); + } + + private int findMethodIndexInVtable(@Nonnull List<Method> vtable, MethodReference method) { for (int i=0; i<vtable.size(); i++) { Method candidate = vtable.get(i); if (MethodUtil.methodSignaturesMatch(candidate, method)) { @@ -391,7 +468,20 @@ public class ClassProto implements TypeProto { return -1; } - @Nonnull SparseArray<FieldReference> getInstanceFields() { + private int findMethodIndexInVtableReverse(@Nonnull List<Method> vtable, MethodReference method) { + for (int i=vtable.size() - 1; i>=0; i--) { + Method candidate = vtable.get(i); + if (MethodUtil.methodSignaturesMatch(candidate, method)) { + if (!classPath.shouldCheckPackagePrivateAccess() || + AnalyzedMethodUtil.canAccess(this, candidate, true, false, false)) { + return i; + } + } + } + return -1; + } + + @Nonnull public SparseArray<FieldReference> getInstanceFields() { if (classPath.isArt()) { return artInstanceFieldsSupplier.get(); } else { @@ -439,10 +529,8 @@ public class ClassProto implements TypeProto { ClassProto superclass = null; if (superclassType != null) { superclass = (ClassProto) classPath.getClass(superclassType); - if (superclass != null) { startFieldOffset = superclass.getNextFieldOffset(); } - } int fieldIndexMod; if ((startFieldOffset % 8) == 0) { @@ -529,14 +617,12 @@ public class ClassProto implements TypeProto { //add padding to align the wide fields, if needed if (fieldTypes[i] == WIDE && !gotDouble) { - if (!gotDouble) { if (fieldOffset % 8 != 0) { assert fieldOffset % 8 == 4; fieldOffset += 4; } gotDouble = true; } - } instanceFields.append(fieldOffset, field); if (fieldTypes[i] == WIDE) { @@ -573,7 +659,7 @@ public class ClassProto implements TypeProto { public static FieldGap newFieldGap(int offset, int size, int oatVersion) { if (oatVersion >= 67) { return new FieldGap(offset, size) { - @Override public int compareTo(FieldGap o) { + @Override public int compareTo(@Nonnull FieldGap o) { int result = Ints.compare(o.size, size); if (result != 0) { return result; @@ -583,7 +669,7 @@ public class ClassProto implements TypeProto { }; } else { return new FieldGap(offset, size) { - @Override public int compareTo(FieldGap o) { + @Override public int compareTo(@Nonnull FieldGap o) { int result = Ints.compare(size, o.size); if (result != 0) { return result; @@ -777,12 +863,18 @@ public class ClassProto implements TypeProto { throw new ExceptionWithContext("Invalid type: %s", type); } - @Nonnull List<Method> getVtable() { - return vtableSupplier.get(); + @Nonnull public List<Method> getVtable() { + if (!classPath.isArt() || classPath.oatVersion < 72) { + return preDefaultMethodVtableSupplier.get(); + } else if (classPath.oatVersion < 87) { + return buggyPostDefaultMethodVtableSupplier.get(); + } else { + return postDefaultMethodVtableSupplier.get(); + } } //TODO: check the case when we have a package private method that overrides an interface method - @Nonnull private final Supplier<List<Method>> vtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() { + @Nonnull private final Supplier<List<Method>> preDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() { @Override public List<Method> get() { List<Method> vtable = Lists.newArrayList(); @@ -811,52 +903,315 @@ public class ClassProto implements TypeProto { //iterate over the virtual methods in the current class, and only add them when we don't already have the //method (i.e. if it was implemented by the superclass) if (!isInterface()) { - addToVtable(getClassDef().getVirtualMethods(), vtable, true); + addToVtable(getClassDef().getVirtualMethods(), vtable, true, true); - // assume that interface method is implemented in the current class, when adding it to vtable - // otherwise it looks like that method is invoked on an interface, which fails Dalvik's optimization checks - for (ClassDef interfaceDef: getDirectInterfaces()) { + // We use the current class for any vtable method references that we add, rather than the interface, so + // we don't end up trying to call invoke-virtual using an interface, which will fail verification + Iterable<ClassDef> interfaces = getDirectInterfaces(); + for (ClassDef interfaceDef: interfaces) { List<Method> interfaceMethods = Lists.newArrayList(); for (Method interfaceMethod: interfaceDef.getVirtualMethods()) { - ImmutableMethod method = new ImmutableMethod( - type, - interfaceMethod.getName(), - interfaceMethod.getParameters(), - interfaceMethod.getReturnType(), - interfaceMethod.getAccessFlags(), - interfaceMethod.getAnnotations(), - interfaceMethod.getImplementation()); - interfaceMethods.add(method); + interfaceMethods.add(new ReparentedMethod(interfaceMethod, type)); } - addToVtable(interfaceMethods, vtable, false); + addToVtable(interfaceMethods, vtable, false, true); } } return vtable; } + }); - private void addToVtable(@Nonnull Iterable<? extends Method> localMethods, - @Nonnull List<Method> vtable, boolean replaceExisting) { - List<? extends Method> methods = Lists.newArrayList(localMethods); - Collections.sort(methods); + /** + * This is the vtable supplier for a version of art that had buggy vtable calculation logic. In some cases it can + * produce multiple vtable entries for a given virtual method. This supplier duplicates this buggy logic in order to + * generate an identical vtable + */ + @Nonnull private final Supplier<List<Method>> buggyPostDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() { + @Override public List<Method> get() { + List<Method> vtable = Lists.newArrayList(); + + //copy the virtual methods from the superclass + String superclassType; + try { + superclassType = getSuperclass(); + } catch (UnresolvedClassException ex) { + vtable.addAll(((ClassProto)classPath.getClass("Ljava/lang/Object;")).getVtable()); + vtableFullyResolved = false; + return vtable; + } + + if (superclassType != null) { + ClassProto superclass = (ClassProto) classPath.getClass(superclassType); + vtable.addAll(superclass.getVtable()); + + // if the superclass's vtable wasn't fully resolved, then we can't know where the new methods added by + // this class should start, so we just propagate what we can from the parent and hope for the best. + if (!superclass.vtableFullyResolved) { + vtableFullyResolved = false; + return vtable; + } + } + + //iterate over the virtual methods in the current class, and only add them when we don't already have the + //method (i.e. if it was implemented by the superclass) + if (!isInterface()) { + addToVtable(getClassDef().getVirtualMethods(), vtable, true, true); + + List<String> interfaces = Lists.newArrayList(getInterfaces().keySet()); + + List<Method> defaultMethods = Lists.newArrayList(); + List<Method> defaultConflictMethods = Lists.newArrayList(); + List<Method> mirandaMethods = Lists.newArrayList(); - outer: for (Method virtualMethod: methods) { - for (int i=0; i<vtable.size(); i++) { - Method superMethod = vtable.get(i); - if (MethodUtil.methodSignaturesMatch(superMethod, virtualMethod)) { + final HashMap<MethodReference, Integer> methodOrder = Maps.newHashMap(); + + for (int i=interfaces.size()-1; i>=0; i--) { + String interfaceType = interfaces.get(i); + ClassDef interfaceDef = classPath.getClassDef(interfaceType); + + for (Method interfaceMethod : interfaceDef.getVirtualMethods()) { + + int vtableIndex = findMethodIndexInVtableReverse(vtable, interfaceMethod); + Method oldVtableMethod = null; + if (vtableIndex >= 0) { + oldVtableMethod = vtable.get(vtableIndex); + } + + for (int j=0; j<vtable.size(); j++) { + Method candidate = vtable.get(j); + if (MethodUtil.methodSignaturesMatch(candidate, interfaceMethod)) { if (!classPath.shouldCheckPackagePrivateAccess() || - AnalyzedMethodUtil.canAccess(ClassProto.this, superMethod, true, false, false)) { - if (replaceExisting) { - vtable.set(i, virtualMethod); + AnalyzedMethodUtil.canAccess(ClassProto.this, candidate, true, false, false)) { + if (interfaceMethodOverrides(interfaceMethod, candidate)) { + vtable.set(j, interfaceMethod); + } + } + } + } + + if (vtableIndex >= 0) { + if (!isOverridableByDefaultMethod(vtable.get(vtableIndex))) { + continue; + } + } + + int defaultMethodIndex = findMethodIndexInVtable(defaultMethods, interfaceMethod); + + if (defaultMethodIndex >= 0) { + if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) { + ClassProto existingInterface = (ClassProto)classPath.getClass( + defaultMethods.get(defaultMethodIndex).getDefiningClass()); + if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) { + Method removedMethod = defaultMethods.remove(defaultMethodIndex); + defaultConflictMethods.add(removedMethod); + } } - continue outer; + continue; } + + int defaultConflictMethodIndex = findMethodIndexInVtable( + defaultConflictMethods, interfaceMethod); + if (defaultConflictMethodIndex >= 0) { + // There's already a matching method in the conflict list, we don't need to do + // anything else + continue; + } + + int mirandaMethodIndex = findMethodIndexInVtable(mirandaMethods, interfaceMethod); + + if (mirandaMethodIndex >= 0) { + if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) { + + ClassProto existingInterface = (ClassProto)classPath.getClass( + mirandaMethods.get(mirandaMethodIndex).getDefiningClass()); + if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) { + Method oldMethod = mirandaMethods.remove(mirandaMethodIndex); + int methodOrderValue = methodOrder.get(oldMethod); + methodOrder.put(interfaceMethod, methodOrderValue); + defaultMethods.add(interfaceMethod); + } + } + continue; + } + + if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) { + if (oldVtableMethod != null) { + if (!interfaceMethodOverrides(interfaceMethod, oldVtableMethod)) { + continue; + } + } + defaultMethods.add(interfaceMethod); + methodOrder.put(interfaceMethod, methodOrder.size()); + } else { + // TODO: do we need to check interfaceMethodOverrides here? + if (oldVtableMethod == null) { + mirandaMethods.add(interfaceMethod); + methodOrder.put(interfaceMethod, methodOrder.size()); + } + } + } + } + + Comparator<MethodReference> comparator = new Comparator<MethodReference>() { + @Override public int compare(MethodReference o1, MethodReference o2) { + return Ints.compare(methodOrder.get(o1), methodOrder.get(o2)); + } + }; + + // The methods should be in the same order within each list as they were iterated over. + // They can be misordered if, e.g. a method was originally added to the default list, but then moved + // to the conflict list. + Collections.sort(mirandaMethods, comparator); + Collections.sort(defaultMethods, comparator); + Collections.sort(defaultConflictMethods, comparator); + + vtable.addAll(mirandaMethods); + vtable.addAll(defaultMethods); + vtable.addAll(defaultConflictMethods); + } + return vtable; + } + }); + + @Nonnull private final Supplier<List<Method>> postDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() { + @Override public List<Method> get() { + List<Method> vtable = Lists.newArrayList(); + + //copy the virtual methods from the superclass + String superclassType; + try { + superclassType = getSuperclass(); + } catch (UnresolvedClassException ex) { + vtable.addAll(((ClassProto)classPath.getClass("Ljava/lang/Object;")).getVtable()); + vtableFullyResolved = false; + return vtable; + } + + if (superclassType != null) { + ClassProto superclass = (ClassProto) classPath.getClass(superclassType); + vtable.addAll(superclass.getVtable()); + + // if the superclass's vtable wasn't fully resolved, then we can't know where the new methods added by + // this class should start, so we just propagate what we can from the parent and hope for the best. + if (!superclass.vtableFullyResolved) { + vtableFullyResolved = false; + return vtable; + } + } + + //iterate over the virtual methods in the current class, and only add them when we don't already have the + //method (i.e. if it was implemented by the superclass) + if (!isInterface()) { + addToVtable(getClassDef().getVirtualMethods(), vtable, true, true); + + Iterable<ClassDef> interfaces = Lists.reverse(Lists.newArrayList(getDirectInterfaces())); + + List<Method> defaultMethods = Lists.newArrayList(); + List<Method> defaultConflictMethods = Lists.newArrayList(); + List<Method> mirandaMethods = Lists.newArrayList(); + + final HashMap<MethodReference, Integer> methodOrder = Maps.newHashMap(); + + for (ClassDef interfaceDef: interfaces) { + for (Method interfaceMethod : interfaceDef.getVirtualMethods()) { + + int vtableIndex = findMethodIndexInVtable(vtable, interfaceMethod); + + if (vtableIndex >= 0) { + if (interfaceMethodOverrides(interfaceMethod, vtable.get(vtableIndex))) { + vtable.set(vtableIndex, interfaceMethod); + } + } else { + int defaultMethodIndex = findMethodIndexInVtable(defaultMethods, interfaceMethod); + + if (defaultMethodIndex >= 0) { + if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) { + ClassProto existingInterface = (ClassProto)classPath.getClass( + defaultMethods.get(defaultMethodIndex).getDefiningClass()); + if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) { + Method removedMethod = defaultMethods.remove(defaultMethodIndex); + defaultConflictMethods.add(removedMethod); + } + } + continue; + } + + int defaultConflictMethodIndex = findMethodIndexInVtable( + defaultConflictMethods, interfaceMethod); + if (defaultConflictMethodIndex >= 0) { + // There's already a matching method in the conflict list, we don't need to do + // anything else + continue; + } + + int mirandaMethodIndex = findMethodIndexInVtable(mirandaMethods, interfaceMethod); + + if (mirandaMethodIndex >= 0) { + if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) { + + ClassProto existingInterface = (ClassProto)classPath.getClass( + mirandaMethods.get(mirandaMethodIndex).getDefiningClass()); + if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) { + Method oldMethod = mirandaMethods.remove(mirandaMethodIndex); + int methodOrderValue = methodOrder.get(oldMethod); + methodOrder.put(interfaceMethod, methodOrderValue); + defaultMethods.add(interfaceMethod); + } + } + continue; + } + + if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) { + defaultMethods.add(interfaceMethod); + methodOrder.put(interfaceMethod, methodOrder.size()); + } else { + mirandaMethods.add(interfaceMethod); + methodOrder.put(interfaceMethod, methodOrder.size()); + } + } + } + } + + Comparator<MethodReference> comparator = new Comparator<MethodReference>() { + @Override public int compare(MethodReference o1, MethodReference o2) { + return Ints.compare(methodOrder.get(o1), methodOrder.get(o2)); } + }; + + // The methods should be in the same order within each list as they were iterated over. + // They can be misordered if, e.g. a method was originally added to the default list, but then moved + // to the conflict list. + Collections.sort(defaultMethods, comparator); + Collections.sort(defaultConflictMethods, comparator); + Collections.sort(mirandaMethods, comparator); + addToVtable(defaultMethods, vtable, false, false); + addToVtable(defaultConflictMethods, vtable, false, false); + addToVtable(mirandaMethods, vtable, false, false); + } + return vtable; + } + }); + + private void addToVtable(@Nonnull Iterable<? extends Method> localMethods, @Nonnull List<Method> vtable, + boolean replaceExisting, boolean sort) { + if (sort) { + ArrayList<Method> methods = Lists.newArrayList(localMethods); + Collections.sort(methods); + localMethods = methods; + } + + for (Method virtualMethod: localMethods) { + int vtableIndex = findMethodIndexInVtable(vtable, virtualMethod); + + if (vtableIndex >= 0) { + if (replaceExisting) { + vtable.set(vtableIndex, virtualMethod); } + } else { // we didn't find an equivalent method, so add it as a new entry vtable.add(virtualMethod); } } - }); + } private static byte getFieldType(@Nonnull FieldReference field) { switch (field.getType().charAt(0)) { @@ -870,4 +1225,68 @@ public class ClassProto implements TypeProto { return 2; //OTHER } } + + private boolean isOverridableByDefaultMethod(@Nonnull Method method) { + ClassProto classProto = (ClassProto)classPath.getClass(method.getDefiningClass()); + return classProto.isInterface(); + } + + /** + * Checks if the interface method overrides the virtual or interface method2 + * @param method A Method from an interface + * @param method2 A Method from an interface or a class + * @return true if the interface method overrides the virtual or interface method2 + */ + private boolean interfaceMethodOverrides(@Nonnull Method method, @Nonnull Method method2) { + ClassProto classProto = (ClassProto)classPath.getClass(method2.getDefiningClass()); + + if (classProto.isInterface()) { + ClassProto targetClassProto = (ClassProto)classPath.getClass(method.getDefiningClass()); + return targetClassProto.implementsInterface(method2.getDefiningClass()); + } else { + return false; + } + } + + static class ReparentedMethod extends BaseMethodReference implements Method { + private final Method method; + private final String definingClass; + + public ReparentedMethod(Method method, String definingClass) { + this.method = method; + this.definingClass = definingClass; + } + + @Nonnull @Override public String getDefiningClass() { + return definingClass; + } + + @Nonnull @Override public String getName() { + return method.getName(); + } + + @Nonnull @Override public List<? extends CharSequence> getParameterTypes() { + return method.getParameterTypes(); + } + + @Nonnull @Override public String getReturnType() { + return method.getReturnType(); + } + + @Nonnull @Override public List<? extends MethodParameter> getParameters() { + return method.getParameters(); + } + + @Override public int getAccessFlags() { + return method.getAccessFlags(); + } + + @Nonnull @Override public Set<? extends Annotation> getAnnotations() { + return method.getAnnotations(); + } + + @Nullable @Override public MethodImplementation getImplementation() { + return method.getImplementation(); + } + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpFields.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpFields.java deleted file mode 100644 index 2bb3e492..00000000 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpFields.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2013, Google Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Google Inc. nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.jf.dexlib2.analysis; - -import com.google.common.base.Splitter; -import com.google.common.collect.Lists; -import org.apache.commons.cli.*; -import org.jf.dexlib2.DexFileFactory; -import org.jf.dexlib2.dexbacked.DexBackedDexFile; -import org.jf.dexlib2.iface.ClassDef; -import org.jf.dexlib2.iface.Field; -import org.jf.dexlib2.iface.Method; -import org.jf.dexlib2.iface.reference.FieldReference; -import org.jf.util.ConsoleUtil; -import org.jf.util.SparseArray; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; - -public class DumpFields { - private static final Options options; - - static { - options = new Options(); - buildOptions(); - } - - public static void main(String[] args) { - CommandLineParser parser = new PosixParser(); - CommandLine commandLine; - - try { - commandLine = parser.parse(options, args); - } catch (ParseException ex) { - usage(); - return; - } - - String[] remainingArgs = commandLine.getArgs(); - - Option[] parsedOptions = commandLine.getOptions(); - ArrayList<String> bootClassPathDirs = Lists.newArrayList(); - String outFile = "fields.txt"; - int apiLevel = 15; - boolean experimental = false; - - for (int i=0; i<parsedOptions.length; i++) { - Option option = parsedOptions[i]; - String opt = option.getOpt(); - - switch (opt.charAt(0)) { - case 'd': - bootClassPathDirs.add(option.getValue()); - break; - case 'o': - outFile = option.getValue(); - break; - case 'a': - apiLevel = Integer.parseInt(commandLine.getOptionValue("a")); - break; - case 'X': - experimental = true; - break; - default: - assert false; - } - } - - if (remainingArgs.length != 1) { - usage(); - return; - } - - String inputDexFileName = remainingArgs[0]; - - File dexFileFile = new File(inputDexFileName); - if (!dexFileFile.exists()) { - System.err.println("Can't find the file " + inputDexFileName); - System.exit(1); - } - - try { - DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexFileFile, apiLevel, experimental); - Iterable<String> bootClassPaths = Splitter.on(":").split("core.jar:ext.jar:framework.jar:android.policy.jar:services.jar"); - ClassPath classPath = ClassPath.fromClassPath(bootClassPathDirs, bootClassPaths, dexFile, apiLevel, experimental); - FileOutputStream outStream = new FileOutputStream(outFile); - - for (ClassDef classDef: dexFile.getClasses()) { - ClassProto classProto = (ClassProto) classPath.getClass(classDef); - SparseArray<FieldReference> fields = classProto.getInstanceFields(); - String className = "Class " + classDef.getType() + " : " + fields.size() + " instance fields\n"; - outStream.write(className.getBytes()); - for (int i=0;i<fields.size();i++) { - String field = fields.keyAt(i) + ":" + fields.valueAt(i).getType() + " " + fields.valueAt(i).getName() + "\n"; - outStream.write(field.getBytes()); - } - outStream.write("\n".getBytes()); - } - outStream.close(); - } catch (IOException ex) { - System.out.println("IOException thrown when trying to open a dex file or write out vtables: " + ex); - } - - } - - /** - * Prints the usage message. - */ - private static void usage() { - int consoleWidth = ConsoleUtil.getConsoleWidth(); - if (consoleWidth <= 0) { - consoleWidth = 80; - } - - System.out.println("java -cp baksmali.jar org.jf.dexlib2.analysis.DumpFields -d path/to/framework/jar/files <dex-file>"); - } - - private static void buildOptions() { - Option classPathDirOption = OptionBuilder.withLongOpt("bootclasspath-dir") - .withDescription("the base folder to look for the bootclasspath files in. Defaults to the current " + - "directory") - .hasArg() - .withArgName("DIR") - .create("d"); - - Option outputFileOption = OptionBuilder.withLongOpt("out-file") - .withDescription("output file") - .hasArg() - .withArgName("FILE") - .create("o"); - - Option apiLevelOption = OptionBuilder.withLongOpt("api-level") - .withDescription("The numeric api-level of the file being disassembled. If not " + - "specified, it defaults to 15 (ICS).") - .hasArg() - .withArgName("API_LEVEL") - .create("a"); - - Option experimentalOption = OptionBuilder.withLongOpt("experimental") - .withDescription("Enable dumping experimental opcodes, that aren't necessarily " + - "supported by the android runtime yet.") - .create("X"); - - options.addOption(classPathDirOption); - options.addOption(outputFileOption); - options.addOption(apiLevelOption); - options.addOption(experimentalOption); - } -} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpVtables.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpVtables.java deleted file mode 100644 index 193c0d39..00000000 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpVtables.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2013, Google Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Google Inc. nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.jf.dexlib2.analysis; - -import com.google.common.base.Splitter; -import com.google.common.collect.Lists; -import org.apache.commons.cli.*; -import org.jf.dexlib2.DexFileFactory; -import org.jf.dexlib2.dexbacked.DexBackedDexFile; -import org.jf.dexlib2.iface.ClassDef; -import org.jf.dexlib2.iface.Method; -import org.jf.util.ConsoleUtil; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -public class DumpVtables { - private static final Options options; - - static { - options = new Options(); - buildOptions(); - } - - public static void main(String[] args) { - CommandLineParser parser = new PosixParser(); - CommandLine commandLine; - - try { - commandLine = parser.parse(options, args); - } catch (ParseException ex) { - usage(); - return; - } - - String[] remainingArgs = commandLine.getArgs(); - - Option[] parsedOptions = commandLine.getOptions(); - ArrayList<String> bootClassPathDirs = Lists.newArrayList(); - String outFile = "vtables.txt"; - int apiLevel = 15; - boolean experimental = false; - - for (int i=0; i<parsedOptions.length; i++) { - Option option = parsedOptions[i]; - String opt = option.getOpt(); - - switch (opt.charAt(0)) { - case 'd': - bootClassPathDirs.add(option.getValue()); - break; - case 'o': - outFile = option.getValue(); - break; - case 'a': - apiLevel = Integer.parseInt(commandLine.getOptionValue("a")); - break; - case 'X': - experimental = true; - break; - default: - assert false; - } - } - - if (remainingArgs.length != 1) { - usage(); - return; - } - - String inputDexFileName = remainingArgs[0]; - - File dexFileFile = new File(inputDexFileName); - if (!dexFileFile.exists()) { - System.err.println("Can't find the file " + inputDexFileName); - System.exit(1); - } - - try { - DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexFileFile, apiLevel, experimental); - Iterable<String> bootClassPaths = Splitter.on(":").split("core.jar:ext.jar:framework.jar:android.policy.jar:services.jar"); - ClassPath classPath = ClassPath.fromClassPath(bootClassPathDirs, bootClassPaths, dexFile, apiLevel, experimental); - FileOutputStream outStream = new FileOutputStream(outFile); - - for (ClassDef classDef: dexFile.getClasses()) { - ClassProto classProto = (ClassProto) classPath.getClass(classDef); - List<Method> methods = classProto.getVtable(); - String className = "Class " + classDef.getType() + " extends " + classDef.getSuperclass() + " : " + methods.size() + " methods\n"; - outStream.write(className.getBytes()); - for (int i=0;i<methods.size();i++) { - Method method = methods.get(i); - - String methodString = i + ":" + method.getDefiningClass() + "->" + method.getName() + "("; - for (CharSequence parameter: method.getParameterTypes()) { - methodString += parameter; - } - methodString += ")" + method.getReturnType() + "\n"; - outStream.write(methodString.getBytes()); - } - outStream.write("\n".getBytes()); - } - outStream.close(); - } catch (IOException ex) { - System.out.println("IOException thrown when trying to open a dex file or write out vtables: " + ex); - } - - } - - /** - * Prints the usage message. - */ - private static void usage() { - int consoleWidth = ConsoleUtil.getConsoleWidth(); - if (consoleWidth <= 0) { - consoleWidth = 80; - } - - System.out.println("java -cp baksmali.jar org.jf.dexlib2.analysis.DumpVtables -d path/to/framework/jar/files <dex-file>"); - } - - private static void buildOptions() { - Option classPathDirOption = OptionBuilder.withLongOpt("bootclasspath-dir") - .withDescription("the base folder to look for the bootclasspath files in. Defaults to the current " + - "directory") - .hasArg() - .withArgName("DIR") - .create("d"); - - Option outputFileOption = OptionBuilder.withLongOpt("out-file") - .withDescription("output file") - .hasArg() - .withArgName("FILE") - .create("o"); - - Option apiLevelOption = OptionBuilder.withLongOpt("api-level") - .withDescription("The numeric api-level of the file being disassembled. If not " + - "specified, it defaults to 15 (ICS).") - .hasArg() - .withArgName("API_LEVEL") - .create("a"); - - Option experimentalOption = OptionBuilder.withLongOpt("experimental") - .withDescription("Enable dumping experimental opcodes, that aren't necessarily " + - "supported by the android runtime yet.") - .create("X"); - - options.addOption(classPathDirOption); - options.addOption(outputFileOption); - options.addOption(apiLevelOption); - options.addOption(experimentalOption); - } -} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java index b7a15a01..7a51c96c 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java @@ -36,6 +36,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.jf.dexlib2.AccessFlags; import org.jf.dexlib2.Opcode; +import org.jf.dexlib2.base.reference.BaseMethodReference; import org.jf.dexlib2.iface.*; import org.jf.dexlib2.iface.instruction.*; import org.jf.dexlib2.iface.instruction.formats.*; @@ -89,10 +90,10 @@ public class MethodAnalyzer { @Nullable private AnalysisException analysisException = null; - //This is a dummy instruction that occurs immediately before the first real instruction. We can initialize the - //register types for this instruction to the parameter types, in order to have them propagate to all of its - //successors, e.g. the first real instruction, the first instructions in any exception handlers covering the first - //instruction, etc. + // This is a dummy instruction that occurs immediately before the first real instruction. We can initialize the + // register types for this instruction to the parameter types, in order to have them propagate to all of its + // successors, e.g. the first real instruction, the first instructions in any exception handlers covering the first + // instruction, etc. private final AnalyzedInstruction startOfMethod; public MethodAnalyzer(@Nonnull ClassPath classPath, @Nonnull Method method, @@ -110,27 +111,16 @@ public class MethodAnalyzer { this.methodImpl = methodImpl; - //override AnalyzedInstruction and provide custom implementations of some of the methods, so that we don't - //have to handle the case this special case of instruction being null, in the main class - startOfMethod = new AnalyzedInstruction(this, null, -1, methodImpl.getRegisterCount()) { - public boolean setsRegister() { - return false; - } - - @Override - public boolean setsWideRegister() { - return false; - } - - @Override - public boolean setsRegister(int registerNumber) { - return false; + // Override AnalyzedInstruction and provide custom implementations of some of the methods, so that we don't + // have to handle the case this special case of instruction being null, in the main class + startOfMethod = new AnalyzedInstruction(this, new ImmutableInstruction10x(Opcode.NOP), -1, methodImpl.getRegisterCount()) { + @Override protected boolean addPredecessor(AnalyzedInstruction predecessor) { + throw new UnsupportedOperationException(); } - @Override - public int getDestinationRegister() { - assert false; - return -1; + @Override @Nonnull + public RegisterType getPredecessorRegisterType(@Nonnull AnalyzedInstruction predecessor, int registerNumber) { + throw new UnsupportedOperationException(); } }; @@ -141,6 +131,7 @@ public class MethodAnalyzer { analyze(); } + @Nonnull public ClassPath getClassPath() { return classPath; } @@ -362,6 +353,7 @@ public class MethodAnalyzer { private void overridePredecessorRegisterTypeAndPropagateChanges( @Nonnull AnalyzedInstruction analyzedInstruction, @Nonnull AnalyzedInstruction predecessor, int registerNumber, @Nonnull RegisterType registerType) { + BitSet changedInstructions = new BitSet(analyzedInstructions.size()); if (!analyzedInstruction.overridePredecessorRegisterType( @@ -383,6 +375,28 @@ public class MethodAnalyzer { } } + private void initializeRefAndPropagateChanges(@Nonnull AnalyzedInstruction analyzedInstruction, + int registerNumber, @Nonnull RegisterType registerType) { + + BitSet changedInstructions = new BitSet(analyzedInstructions.size()); + + if (!analyzedInstruction.setPostRegisterType(registerNumber, registerType)) { + return; + } + + propagateRegisterToSuccessors(analyzedInstruction, registerNumber, changedInstructions, false); + + propagateChanges(changedInstructions, registerNumber, false); + + if (registerType.category == RegisterType.LONG_LO) { + checkWidePair(registerNumber, analyzedInstruction); + setPostRegisterTypeAndPropagateChanges(analyzedInstruction, registerNumber+1, RegisterType.LONG_HI_TYPE); + } else if (registerType.category == RegisterType.DOUBLE_LO) { + checkWidePair(registerNumber, analyzedInstruction); + setPostRegisterTypeAndPropagateChanges(analyzedInstruction, registerNumber+1, RegisterType.DOUBLE_HI_TYPE); + } + } + private void setPostRegisterTypeAndPropagateChanges(@Nonnull AnalyzedInstruction analyzedInstruction, int registerNumber, @Nonnull RegisterType registerType) { @@ -1176,32 +1190,46 @@ public class MethodAnalyzer { setDestinationRegisterTypeAndPropagateChanges(analyzedInstruction, castRegisterType); } - static boolean canNarrowAfterInstanceOf(AnalyzedInstruction analyzedInstanceOfInstruction, - AnalyzedInstruction analyzedIfInstruction, ClassPath classPath) { + public static boolean isNotWideningConversion(RegisterType originalType, RegisterType newType) { + if (originalType.type == null || newType.type == null) { + return true; + } + if (originalType.type.isInterface()) { + return newType.type.implementsInterface(originalType.type.getType()); + } else { + TypeProto commonSuperclass = newType.type.getCommonSuperclass(originalType.type); + if (commonSuperclass.getType().equals(originalType.type.getType())) { + return true; + } + if (commonSuperclass.getType().equals(newType.type.getType())) { + return false; + } + } + return true; + } + + static boolean canPropagateTypeAfterInstanceOf(AnalyzedInstruction analyzedInstanceOfInstruction, + AnalyzedInstruction analyzedIfInstruction, ClassPath classPath) { + if (!classPath.isArt()) { + return false; + } + Instruction ifInstruction = analyzedIfInstruction.instruction; - assert analyzedIfInstruction.instruction != null; if (((Instruction21t)ifInstruction).getRegisterA() == analyzedInstanceOfInstruction.getDestinationRegister()) { Reference reference = ((Instruction22c)analyzedInstanceOfInstruction.getInstruction()).getReference(); RegisterType registerType = RegisterType.getRegisterType(classPath, (TypeReference)reference); - if (registerType.type != null && !registerType.type.isInterface()) { - int objectRegister = ((TwoRegisterInstruction)analyzedInstanceOfInstruction.getInstruction()) - .getRegisterB(); + try { + if (registerType.type != null && !registerType.type.isInterface()) { + int objectRegister = ((TwoRegisterInstruction)analyzedInstanceOfInstruction.getInstruction()) + .getRegisterB(); - RegisterType originalType = analyzedIfInstruction.getPreInstructionRegisterType(objectRegister); + RegisterType originalType = analyzedIfInstruction.getPreInstructionRegisterType(objectRegister); - if (originalType.type != null) { - // Only override if we're going from an interface to a class, or are going to a narrower class - if (originalType.type.isInterface()) { - return true; - } else { - TypeProto commonSuperclass = registerType.type.getCommonSuperclass(originalType.type); - // only if it's a narrowing conversion - if (commonSuperclass.getType().equals(originalType.type.getType())) { - return true; - } - } + return isNotWideningConversion(originalType, registerType); } + } catch (UnresolvedClassException ex) { + return false; } } return false; @@ -1210,21 +1238,16 @@ public class MethodAnalyzer { /** * Art uses a peephole optimization for an if-eqz or if-nez that occur immediately after an instance-of. It will * narrow the type if possible, and then NOP out any corresponding check-cast instruction later on - * - * TODO: Is this still safe to do even for dalvik odexes? I think it should be.. */ private void analyzeIfEqzNez(@Nonnull AnalyzedInstruction analyzedInstruction) { - int instructionIndex = analyzedInstruction.getInstructionIndex(); - if (instructionIndex > 0) { - AnalyzedInstruction prevAnalyzedInstruction = analyzedInstructions.valueAt(instructionIndex - 1); - if (prevAnalyzedInstruction.instruction != null && - prevAnalyzedInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF) { - if (canNarrowAfterInstanceOf(prevAnalyzedInstruction, analyzedInstruction, classPath)) { - // Propagate the original type to the failing branch, and the new type to the successful branch - int narrowingRegister = ((Instruction22c)prevAnalyzedInstruction.instruction).getRegisterB(); - RegisterType originalType = analyzedInstruction.getPreInstructionRegisterType(narrowingRegister); - RegisterType newType = RegisterType.getRegisterType(classPath, - (TypeReference)((Instruction22c)prevAnalyzedInstruction.instruction).getReference()); + if (classPath.isArt()) { + int instructionIndex = analyzedInstruction.getInstructionIndex(); + if (instructionIndex > 0) { + if (analyzedInstruction.getPredecessorCount() != 1) { + return; + } + AnalyzedInstruction prevAnalyzedInstruction = analyzedInstruction.getPredecessors().first(); + if (prevAnalyzedInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF) { AnalyzedInstruction fallthroughInstruction = analyzedInstructions.valueAt( analyzedInstruction.getInstructionIndex() + 1); @@ -1233,16 +1256,25 @@ public class MethodAnalyzer { ((Instruction21t)analyzedInstruction.instruction).getCodeOffset(); AnalyzedInstruction branchInstruction = analyzedInstructions.get(nextAddress); - if (analyzedInstruction.instruction.getOpcode() == Opcode.IF_EQZ) { - overridePredecessorRegisterTypeAndPropagateChanges(fallthroughInstruction, analyzedInstruction, - narrowingRegister, newType); - overridePredecessorRegisterTypeAndPropagateChanges(branchInstruction, analyzedInstruction, - narrowingRegister, originalType); - } else { - overridePredecessorRegisterTypeAndPropagateChanges(fallthroughInstruction, analyzedInstruction, - narrowingRegister, originalType); - overridePredecessorRegisterTypeAndPropagateChanges(branchInstruction, analyzedInstruction, - narrowingRegister, newType); + int narrowingRegister = ((Instruction22c)prevAnalyzedInstruction.instruction).getRegisterB(); + RegisterType originalType = analyzedInstruction.getPreInstructionRegisterType(narrowingRegister); + + Instruction22c instanceOfInstruction = (Instruction22c)prevAnalyzedInstruction.instruction; + RegisterType newType = RegisterType.getRegisterType(classPath, + (TypeReference)instanceOfInstruction.getReference()); + + for (int register : analyzedInstruction.getSetRegisters()) { + if (analyzedInstruction.instruction.getOpcode() == Opcode.IF_EQZ) { + overridePredecessorRegisterTypeAndPropagateChanges(fallthroughInstruction, + analyzedInstruction, register, newType); + overridePredecessorRegisterTypeAndPropagateChanges(branchInstruction, analyzedInstruction, + register, originalType); + } else { + overridePredecessorRegisterTypeAndPropagateChanges(fallthroughInstruction, + analyzedInstruction, register, originalType); + overridePredecessorRegisterTypeAndPropagateChanges(branchInstruction, analyzedInstruction, + register, newType); + } } } } @@ -1380,44 +1412,32 @@ public class MethodAnalyzer { } private void analyzeInvokeDirectCommon(@Nonnull AnalyzedInstruction analyzedInstruction, int objectRegister) { - //the only time that an invoke instruction changes a register type is when using invoke-direct on a - //constructor (<init>) method, which changes the uninitialized reference (and any register that the same - //uninit reference has been copied to) to an initialized reference - - ReferenceInstruction instruction = (ReferenceInstruction)analyzedInstruction.instruction; - - MethodReference methodReference = (MethodReference)instruction.getReference(); - - if (!methodReference.getName().equals("<init>")) { - return; - } - - RegisterType objectRegisterType = analyzedInstruction.getPreInstructionRegisterType(objectRegister); - - if (objectRegisterType.category != RegisterType.UNINIT_REF && - objectRegisterType.category != RegisterType.UNINIT_THIS) { - return; - } + // This handles the case of invoking a constructor on an uninitialized reference. This propagates the + // initialized type for the object register, and also any known aliased registers. + // + // In some cases, unrelated uninitialized references may not have been propagated past this instruction. This + // happens when propagating those types and the type of object register of this instruction isn't known yet. + // In this case, we can't determine if the uninitialized reference being propagated in an alias of the object + // register, so we don't stop propagation. + // + // We check for any of these unpropagated uninitialized references here and propagate them. + if (analyzedInstruction.isInvokeInit()) { + RegisterType uninitRef = analyzedInstruction.getPreInstructionRegisterType(objectRegister); + if (uninitRef.category != RegisterType.UNINIT_REF && uninitRef.category != RegisterType.UNINIT_THIS) { + assert analyzedInstruction.getSetRegisters().isEmpty(); + return; + } - setPostRegisterTypeAndPropagateChanges(analyzedInstruction, objectRegister, - RegisterType.getRegisterType(RegisterType.REFERENCE, objectRegisterType.type)); + RegisterType initRef = RegisterType.getRegisterType(RegisterType.REFERENCE, uninitRef.type); - for (int i=0; i<analyzedInstruction.postRegisterMap.length; i++) { - RegisterType postInstructionRegisterType = analyzedInstruction.postRegisterMap[i]; - if (postInstructionRegisterType.category == RegisterType.UNKNOWN) { - RegisterType preInstructionRegisterType = - analyzedInstruction.getPreInstructionRegisterType(i); + for (int register: analyzedInstruction.getSetRegisters()) { + RegisterType registerType = analyzedInstruction.getPreInstructionRegisterType(register); - if (preInstructionRegisterType.category == RegisterType.UNINIT_REF || - preInstructionRegisterType.category == RegisterType.UNINIT_THIS) { - RegisterType registerType; - if (preInstructionRegisterType.equals(objectRegisterType)) { - registerType = analyzedInstruction.postRegisterMap[objectRegister]; - } else { - registerType = preInstructionRegisterType; - } - - setPostRegisterTypeAndPropagateChanges(analyzedInstruction, i, registerType); + if (registerType == uninitRef) { + setPostRegisterTypeAndPropagateChanges(analyzedInstruction, register, initRef); + } else { + // This is unrelated uninitialized reference. propagate it as-is + setPostRegisterTypeAndPropagateChanges(analyzedInstruction, register, registerType); } } } @@ -1695,13 +1715,13 @@ public class MethodAnalyzer { // fieldClass is now the first accessible class found. Now. we need to make sure that the field is // actually valid for this class - resolvedField = classPath.getClass(fieldClass.getType()).getFieldByOffset(fieldOffset); - if (resolvedField == null) { + FieldReference newResolvedField = classPath.getClass(fieldClass.getType()).getFieldByOffset(fieldOffset); + if (newResolvedField == null) { throw new ExceptionWithContext("Couldn't find accessible class while resolving field %s", ReferenceUtil.getShortFieldDescriptor(resolvedField)); } - resolvedField = new ImmutableFieldReference(fieldClass.getType(), resolvedField.getName(), - resolvedField.getType()); + resolvedField = new ImmutableFieldReference(fieldClass.getType(), newResolvedField.getName(), + newResolvedField.getType()); } String fieldType = resolvedField.getType(); @@ -1733,41 +1753,9 @@ public class MethodAnalyzer { targetMethod = (MethodReference)instruction.getReference(); } - TypeProto typeProto = classPath.getClass(targetMethod.getDefiningClass()); - int methodIndex; - try { - methodIndex = typeProto.findMethodIndexInVtable(targetMethod); - } catch (UnresolvedClassException ex) { - return true; - } - - if (methodIndex < 0) { - return true; - } - - Method replacementMethod = typeProto.getMethodByVtableIndex(methodIndex); - assert replacementMethod != null; - while (true) { - String superType = typeProto.getSuperclass(); - if (superType == null) { - break; - } - typeProto = classPath.getClass(superType); - Method resolvedMethod = typeProto.getMethodByVtableIndex(methodIndex); - if (resolvedMethod == null) { - break; - } - - if (!resolvedMethod.equals(replacementMethod)) { - if (!AnalyzedMethodUtil.canAccess(typeProto, replacementMethod, true, true, true)) { - continue; - } - - replacementMethod = resolvedMethod; - } - } + MethodReference replacementMethod = normalizeMethodReference(targetMethod); - if (replacementMethod.equals(method)) { + if (replacementMethod == null || replacementMethod.equals(targetMethod)) { return true; } @@ -1839,7 +1827,9 @@ public class MethodAnalyzer { // no need to check class access for invoke-super. A class can obviously access its superclass. ClassDef thisClass = classPath.getClassDef(method.getDefiningClass()); - if (!isSuper && !TypeUtils.canAccessClass( + if (classPath.getClass(resolvedMethod.getDefiningClass()).isInterface()) { + resolvedMethod = new ReparentedMethodReference(resolvedMethod, objectRegisterTypeProto.getType()); + } else if (!isSuper && !TypeUtils.canAccessClass( thisClass.getType(), classPath.getClassDef(resolvedMethod.getDefiningClass()))) { // the class is not accessible. So we start looking at objectRegisterTypeProto (which may be different @@ -1860,13 +1850,20 @@ public class MethodAnalyzer { MethodReference newResolvedMethod = classPath.getClass(methodClass.getType()).getMethodByVtableIndex(methodIndex); if (newResolvedMethod == null) { - // TODO: fix NPE here throw new ExceptionWithContext("Couldn't find accessible class while resolving method %s", ReferenceUtil.getMethodDescriptor(resolvedMethod, true)); } resolvedMethod = newResolvedMethod; resolvedMethod = new ImmutableMethodReference(methodClass.getType(), resolvedMethod.getName(), resolvedMethod.getParameterTypes(), resolvedMethod.getReturnType()); + + } + + if (normalizeVirtualMethods) { + MethodReference replacementMethod = normalizeMethodReference(resolvedMethod); + if (replacementMethod != null) { + resolvedMethod = replacementMethod; + } } Instruction deodexedInstruction; @@ -1967,4 +1964,70 @@ public class MethodAnalyzer { "pair because it is the last register.", registerNumber)); } } + + @Nullable + private MethodReference normalizeMethodReference(@Nonnull MethodReference methodRef) { + TypeProto typeProto = classPath.getClass(methodRef.getDefiningClass()); + int methodIndex; + try { + methodIndex = typeProto.findMethodIndexInVtable(methodRef); + } catch (UnresolvedClassException ex) { + return null; + } + + if (methodIndex < 0) { + return null; + } + + ClassProto thisClass = (ClassProto)classPath.getClass(method.getDefiningClass()); + + Method replacementMethod = typeProto.getMethodByVtableIndex(methodIndex); + assert replacementMethod != null; + while (true) { + String superType = typeProto.getSuperclass(); + if (superType == null) { + break; + } + typeProto = classPath.getClass(superType); + Method resolvedMethod = typeProto.getMethodByVtableIndex(methodIndex); + if (resolvedMethod == null) { + break; + } + + if (!resolvedMethod.equals(replacementMethod)) { + if (!AnalyzedMethodUtil.canAccess(thisClass, resolvedMethod, false, false, true)) { + continue; + } + + replacementMethod = resolvedMethod; + } + } + return replacementMethod; + } + + private static class ReparentedMethodReference extends BaseMethodReference { + private final MethodReference baseReference; + private final String definingClass; + + public ReparentedMethodReference(MethodReference baseReference, String definingClass) { + this.baseReference = baseReference; + this.definingClass = definingClass; + } + + @Override @Nonnull public String getName() { + return baseReference.getName(); + } + + @Override @Nonnull public List<? extends CharSequence> getParameterTypes() { + return baseReference.getParameterTypes(); + } + + @Override @Nonnull public String getReturnType() { + return baseReference.getReturnType(); + } + + @Nonnull @Override public String getDefiningClass() { + return definingClass; + } + } }
\ No newline at end of file diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/RegisterType.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/RegisterType.java index ba782fe6..75478ca6 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/RegisterType.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/RegisterType.java @@ -235,7 +235,7 @@ public class RegisterType { case '[': return getRegisterType(REFERENCE, classPath.getClass(type)); default: - throw new ExceptionWithContext("Invalid type: " + type); + throw new AnalysisException("Invalid type: " + type); } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/reflection/util/ReflectionUtils.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/reflection/util/ReflectionUtils.java index 4a4615a6..029ddb9a 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/reflection/util/ReflectionUtils.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/reflection/util/ReflectionUtils.java @@ -31,12 +31,43 @@ package org.jf.dexlib2.analysis.reflection.util; +import com.google.common.collect.ImmutableBiMap; + public class ReflectionUtils { + + private static ImmutableBiMap<String, String> primitiveMap = ImmutableBiMap.<String, String>builder() + .put("boolean", "Z") + .put("int", "I") + .put("long", "J") + .put("double", "D") + .put("void", "V") + .put("float", "F") + .put("char", "C") + .put("short", "S") + .put("byte", "B") + .build(); + public static String javaToDexName(String javaName) { - javaName = javaName.replace('.', '/'); - if (javaName.length() > 1 && javaName.charAt(javaName.length()-1) != ';') { - javaName = 'L' + javaName + ';'; + if (javaName.charAt(0) == '[') { + return javaName.replace('.', '/'); + } + + if (primitiveMap.containsKey(javaName)) { + return primitiveMap.get(javaName); + } + + return 'L' + javaName.replace('.', '/') + ';'; + } + + public static String dexToJavaName(String dexName) { + if (dexName.charAt(0) == '[') { + return dexName.replace('/', '.'); } - return javaName; + + if (primitiveMap.inverse().containsKey(dexName)) { + return primitiveMap.inverse().get(dexName); + } + + return dexName.replace('/', '.').substring(1, dexName.length()-2); } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseFieldReference.java b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseFieldReference.java index f056f245..862e342b 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseFieldReference.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseFieldReference.java @@ -32,6 +32,7 @@ package org.jf.dexlib2.base.reference; import org.jf.dexlib2.iface.reference.FieldReference; +import org.jf.dexlib2.util.ReferenceUtil; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -64,4 +65,8 @@ public abstract class BaseFieldReference implements FieldReference { if (res != 0) return res; return getType().compareTo(o.getType()); } + + @Override public String toString() { + return ReferenceUtil.getFieldDescriptor(this); + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodProtoReference.java b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodProtoReference.java index c0d38b0b..2fc5ed13 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodProtoReference.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodProtoReference.java @@ -33,6 +33,7 @@ package org.jf.dexlib2.base.reference; import com.google.common.collect.Ordering; import org.jf.dexlib2.iface.reference.MethodProtoReference; +import org.jf.dexlib2.util.ReferenceUtil; import org.jf.util.CharSequenceUtils; import org.jf.util.CollectionUtils; @@ -63,4 +64,8 @@ public abstract class BaseMethodProtoReference implements MethodProtoReference { if (res != 0) return res; return CollectionUtils.compareAsIterable(Ordering.usingToString(), getParameterTypes(), o.getParameterTypes()); } + + @Override public String toString() { + return ReferenceUtil.getMethodProtoDescriptor(this); + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodReference.java b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodReference.java index 3ff6f7db..f297760e 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodReference.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodReference.java @@ -33,6 +33,7 @@ package org.jf.dexlib2.base.reference; import com.google.common.collect.Ordering; import org.jf.dexlib2.iface.reference.MethodReference; +import org.jf.dexlib2.util.ReferenceUtil; import org.jf.util.CharSequenceUtils; import org.jf.util.CollectionUtils; @@ -70,4 +71,8 @@ public abstract class BaseMethodReference implements MethodReference { if (res != 0) return res; return CollectionUtils.compareAsIterable(Ordering.usingToString(), getParameterTypes(), o.getParameterTypes()); } + + @Override public String toString() { + return ReferenceUtil.getMethodDescriptor(this); + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseStringReference.java b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseStringReference.java index c6daa91e..2f13c1ae 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseStringReference.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseStringReference.java @@ -58,5 +58,5 @@ public abstract class BaseStringReference implements StringReference { @Override public int length() { return getString().length(); } @Override public char charAt(int index) { return getString().charAt(index); } @Override public CharSequence subSequence(int start, int end) { return getString().subSequence(start, end); } - @Override public String toString() { return getString(); } + @Override @Nonnull public String toString() { return getString(); } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/builder/BuilderOffsetInstruction.java b/dexlib2/src/main/java/org/jf/dexlib2/builder/BuilderOffsetInstruction.java index d75d7b67..27e43d5e 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/builder/BuilderOffsetInstruction.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/builder/BuilderOffsetInstruction.java @@ -33,6 +33,7 @@ package org.jf.dexlib2.builder; import org.jf.dexlib2.Opcode; import org.jf.dexlib2.iface.instruction.OffsetInstruction; +import org.jf.util.ExceptionWithContext; import javax.annotation.Nonnull; @@ -48,9 +49,16 @@ public abstract class BuilderOffsetInstruction extends BuilderInstruction implem @Override public int getCodeOffset() { int codeOffset = internalGetCodeOffset(); - if ((this.getCodeUnits() == 1 && (codeOffset < Byte.MIN_VALUE || codeOffset > Byte.MAX_VALUE)) || - (this.getCodeUnits() == 2 && (codeOffset < Short.MIN_VALUE || codeOffset > Short.MAX_VALUE))) { - throw new IllegalStateException("Target is out of range"); + if (this.getCodeUnits() == 1) { + if (codeOffset < Byte.MIN_VALUE || codeOffset > Byte.MAX_VALUE) { + throw new ExceptionWithContext("Invalid instruction offset: %d. " + + "Offset must be in [-128, 127]", codeOffset); + } + } else if (this.getCodeUnits() == 2) { + if (codeOffset < Short.MIN_VALUE || codeOffset > Short.MAX_VALUE) { + throw new ExceptionWithContext("Invalid instruction offset: %d. " + + "Offset must be in [-32768, 32767]", codeOffset); + } } return codeOffset; } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java index 32505eec..fe260c5f 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java @@ -33,16 +33,24 @@ package org.jf.dexlib2.dexbacked; import com.google.common.io.ByteStreams; import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.ReferenceType; import org.jf.dexlib2.dexbacked.raw.*; +import org.jf.dexlib2.dexbacked.reference.DexBackedFieldReference; +import org.jf.dexlib2.dexbacked.reference.DexBackedMethodReference; +import org.jf.dexlib2.dexbacked.reference.DexBackedStringReference; +import org.jf.dexlib2.dexbacked.reference.DexBackedTypeReference; import org.jf.dexlib2.dexbacked.util.FixedSizeSet; import org.jf.dexlib2.iface.DexFile; +import org.jf.dexlib2.iface.reference.Reference; +import org.jf.dexlib2.util.DexUtil; import org.jf.util.ExceptionWithContext; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.util.AbstractList; +import java.util.List; import java.util.Set; public class DexBackedDexFile extends BaseDexBuffer implements DexFile { @@ -61,13 +69,13 @@ public class DexBackedDexFile extends BaseDexBuffer implements DexFile { private final int classCount; private final int classStartOffset; - private DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, int offset, boolean verifyMagic) { + protected DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, int offset, boolean verifyMagic) { super(buf, offset); this.opcodes = opcodes; if (verifyMagic) { - verifyMagicAndByteOrder(buf, offset); + DexUtil.verifyDexHeader(buf, offset); } stringCount = readSmallUint(HeaderItem.STRING_COUNT_OFFSET); @@ -85,7 +93,7 @@ public class DexBackedDexFile extends BaseDexBuffer implements DexFile { } public DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull BaseDexBuffer buf) { - this(opcodes, buf.buf); + this(opcodes, buf.buf, buf.baseOffset); } public DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, int offset) { @@ -96,22 +104,10 @@ public class DexBackedDexFile extends BaseDexBuffer implements DexFile { this(opcodes, buf, 0, true); } + @Nonnull public static DexBackedDexFile fromInputStream(@Nonnull Opcodes opcodes, @Nonnull InputStream is) throws IOException { - if (!is.markSupported()) { - throw new IllegalArgumentException("InputStream must support mark"); - } - is.mark(44); - byte[] partialHeader = new byte[44]; - try { - ByteStreams.readFully(is, partialHeader); - } catch (EOFException ex) { - throw new NotADexFile("File is too short"); - } finally { - is.reset(); - } - - verifyMagicAndByteOrder(partialHeader, 0); + DexUtil.verifyDexHeader(is); byte[] buf = ByteStreams.toByteArray(is); return new DexBackedDexFile(opcodes, buf, 0, false); @@ -148,25 +144,6 @@ public class DexBackedDexFile extends BaseDexBuffer implements DexFile { }; } - private static void verifyMagicAndByteOrder(@Nonnull byte[] buf, int offset) { - if (!HeaderItem.verifyMagic(buf, offset)) { - StringBuilder sb = new StringBuilder("Invalid magic value:"); - for (int i=0; i<8; i++) { - sb.append(String.format(" %02x", buf[i])); - } - throw new NotADexFile(sb.toString()); - } - - int endian = HeaderItem.getEndian(buf, offset); - if (endian == HeaderItem.BIG_ENDIAN_TAG) { - throw new ExceptionWithContext("Big endian dex files are not currently supported"); - } - - if (endian != HeaderItem.LITTLE_ENDIAN_TAG) { - throw new ExceptionWithContext("Invalid endian tag: 0x%x", endian); - } - } - public int getStringIdItemOffset(int stringIndex) { if (stringIndex < 0 || stringIndex >= stringCount) { throw new InvalidItemIndex(stringIndex, "String index out of bounds: %d", stringIndex); @@ -265,6 +242,81 @@ public class DexBackedDexFile extends BaseDexBuffer implements DexFile { return getType(typeIndex); } + public List<DexBackedStringReference> getStrings() { + return new AbstractList<DexBackedStringReference>() { + @Override public DexBackedStringReference get(int index) { + if (index < 0 || index >= getStringCount()) { + throw new IndexOutOfBoundsException(); + } + return new DexBackedStringReference(DexBackedDexFile.this, index); + } + + @Override public int size() { + return getStringCount(); + } + }; + } + + public List<DexBackedTypeReference> getTypes() { + return new AbstractList<DexBackedTypeReference>() { + @Override public DexBackedTypeReference get(int index) { + if (index < 0 || index >= getTypeCount()) { + throw new IndexOutOfBoundsException(); + } + return new DexBackedTypeReference(DexBackedDexFile.this, index); + } + + @Override public int size() { + return getTypeCount(); + } + }; + } + + public List<DexBackedMethodReference> getMethods() { + return new AbstractList<DexBackedMethodReference>() { + @Override public DexBackedMethodReference get(int index) { + if (index < 0 || index >= getMethodCount()) { + throw new IndexOutOfBoundsException(); + } + return new DexBackedMethodReference(DexBackedDexFile.this, index); + } + + @Override public int size() { + return getMethodCount(); + } + }; + } + + public List<DexBackedFieldReference> getFields() { + return new AbstractList<DexBackedFieldReference>() { + @Override public DexBackedFieldReference get(int index) { + if (index < 0 || index >= getFieldCount()) { + throw new IndexOutOfBoundsException(); + } + return new DexBackedFieldReference(DexBackedDexFile.this, index); + } + + @Override public int size() { + return getFieldCount(); + } + }; + } + + public List<? extends Reference> getReferences(int referenceType) { + switch (referenceType) { + case ReferenceType.STRING: + return getStrings(); + case ReferenceType.TYPE: + return getTypes(); + case ReferenceType.METHOD: + return getMethods(); + case ReferenceType.FIELD: + return getFields(); + default: + throw new IllegalArgumentException(String.format("Invalid reference type: %d", referenceType)); + } + } + @Override @Nonnull public DexReader readerAt(int offset) { diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java index 676d86cd..a82032a1 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java @@ -84,7 +84,7 @@ public class DexBackedMethodImplementation implements MethodImplementation { // Does the instruction extend past the end of the method? int offset = reader.getOffset(); if (offset > endOffset || offset < 0) { - throw new ExceptionWithContext("The last instruction in the method is truncated"); + throw new ExceptionWithContext("The last instruction in method %s is truncated", method); } return instruction; } @@ -129,7 +129,11 @@ public class DexBackedMethodImplementation implements MethodImplementation { return DebugInfo.newOrEmpty(dexFile, 0, this); } if (debugOffset < 0) { - System.err.println("%s: Invalid debug offset"); + System.err.println(String.format("%s: Invalid debug offset", method)); + return DebugInfo.newOrEmpty(dexFile, 0, this); + } + if (debugOffset >= dexFile.buf.length) { + System.err.println(String.format("%s: Invalid debug offset", method)); return DebugInfo.newOrEmpty(dexFile, 0, this); } return DebugInfo.newOrEmpty(dexFile, debugOffset, this); diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedOdexFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedOdexFile.java index 12f19db0..379ecaac 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedOdexFile.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedOdexFile.java @@ -35,9 +35,9 @@ import com.google.common.io.ByteStreams; import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.dexbacked.raw.OdexHeaderItem; import org.jf.dexlib2.dexbacked.util.VariableSizeList; +import org.jf.dexlib2.util.DexUtil; import javax.annotation.Nonnull; -import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; @@ -49,7 +49,6 @@ public class DexBackedOdexFile extends DexBackedDexFile { private final byte[] odexBuf; - public DexBackedOdexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] odexBuf, byte[] dexBuf) { super(opcodes, dexBuf); @@ -64,7 +63,7 @@ public class DexBackedOdexFile extends DexBackedDexFile { return true; } - public List<String> getDependencies() { + @Nonnull public List<String> getDependencies() { final int dexOffset = OdexHeaderItem.getDexOffset(odexBuf); final int dependencyOffset = OdexHeaderItem.getDependenciesOffset(odexBuf) - dexOffset; @@ -85,22 +84,9 @@ public class DexBackedOdexFile extends DexBackedDexFile { }; } - public static DexBackedOdexFile fromInputStream(@Nonnull Opcodes opcodes, @Nonnull InputStream is) + @Nonnull public static DexBackedOdexFile fromInputStream(@Nonnull Opcodes opcodes, @Nonnull InputStream is) throws IOException { - if (!is.markSupported()) { - throw new IllegalArgumentException("InputStream must support mark"); - } - is.mark(8); - byte[] partialHeader = new byte[8]; - try { - ByteStreams.readFully(is, partialHeader); - } catch (EOFException ex) { - throw new NotADexFile("File is too short"); - } finally { - is.reset(); - } - - verifyMagic(partialHeader); + DexUtil.verifyOdexHeader(is); is.reset(); byte[] odexBuf = new byte[OdexHeaderItem.ITEM_SIZE]; @@ -115,18 +101,8 @@ public class DexBackedOdexFile extends DexBackedDexFile { return new DexBackedOdexFile(opcodes, odexBuf, dexBuf); } - private static void verifyMagic(byte[] buf) { - if (!OdexHeaderItem.verifyMagic(buf)) { - StringBuilder sb = new StringBuilder("Invalid magic value:"); - for (int i=0; i<8; i++) { - sb.append(String.format(" %02x", buf[i])); - } - throw new NotAnOdexFile(sb.toString()); - } - } - public int getOdexVersion() { - return OdexHeaderItem.getVersion(odexBuf); + return OdexHeaderItem.getVersion(odexBuf, 0); } public static class NotAnOdexFile extends RuntimeException { diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java index dbeb67ce..aaf942ea 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java @@ -31,22 +31,29 @@ package org.jf.dexlib2.dexbacked; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterators; import com.google.common.io.ByteStreams; import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.OatFile.OatDexFile; import org.jf.dexlib2.dexbacked.OatFile.SymbolTable.Symbol; import org.jf.dexlib2.dexbacked.raw.HeaderItem; +import org.jf.dexlib2.iface.MultiDexContainer; import org.jf.util.AbstractForwardSequentialList; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.util.AbstractList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; -public class OatFile extends BaseDexBuffer { +public class OatFile extends BaseDexBuffer implements MultiDexContainer<OatDexFile> { private static final byte[] ELF_MAGIC = new byte[] { 0x7f, 'E', 'L', 'F' }; private static final byte[] OAT_MAGIC = new byte[] { 'o', 'a', 't', '\n' }; private static final int MIN_ELF_HEADER_SIZE = 52; @@ -54,7 +61,7 @@ public class OatFile extends BaseDexBuffer { // These are the "known working" versions that I have manually inspected the source for. // Later version may or may not work, depending on what changed. private static final int MIN_OAT_VERSION = 56; - private static final int MAX_OAT_VERSION = 71; + private static final int MAX_OAT_VERSION = 86; public static final int UNSUPPORTED = 0; public static final int SUPPORTED = 1; @@ -63,8 +70,13 @@ public class OatFile extends BaseDexBuffer { private final boolean is64bit; @Nonnull private final OatHeader oatHeader; @Nonnull private final Opcodes opcodes; + @Nullable private final VdexProvider vdexProvider; public OatFile(@Nonnull byte[] buf) { + this(buf, null); + } + + public OatFile(@Nonnull byte[] buf, @Nullable VdexProvider vdexProvider) { super(buf); if (buf.length < MIN_ELF_HEADER_SIZE) { @@ -100,6 +112,7 @@ public class OatFile extends BaseDexBuffer { } this.opcodes = Opcodes.forArtVersion(oatHeader.getVersion()); + this.vdexProvider = vdexProvider; } private static void verifyMagic(byte[] buf) { @@ -110,7 +123,11 @@ public class OatFile extends BaseDexBuffer { } } - public static OatFile fromInputStream(@Nonnull InputStream is) + public static OatFile fromInputStream(@Nonnull InputStream is) throws IOException { + return fromInputStream(is, null); + } + + public static OatFile fromInputStream(@Nonnull InputStream is, @Nullable VdexProvider vdexProvider) throws IOException { if (!is.markSupported()) { throw new IllegalArgumentException("InputStream must support mark"); @@ -130,7 +147,7 @@ public class OatFile extends BaseDexBuffer { is.reset(); byte[] buf = ByteStreams.toByteArray(is); - return new OatFile(buf); + return new OatFile(buf, vdexProvider); } public int getOatVersion() { @@ -149,6 +166,22 @@ public class OatFile extends BaseDexBuffer { } @Nonnull + public List<String> getBootClassPath() { + if (getOatVersion() < 75) { + return ImmutableList.of(); + } + String bcp = oatHeader.getKeyValue("bootclasspath"); + if (bcp == null) { + return ImmutableList.of(); + } + return Arrays.asList(bcp.split(":")); + } + + @Nonnull @Override public Opcodes getOpcodes() { + return opcodes; + } + + @Nonnull public List<OatDexFile> getDexFiles() { return new AbstractForwardSequentialList<OatDexFile>() { @Override public int size() { @@ -156,53 +189,57 @@ public class OatFile extends BaseDexBuffer { } @Nonnull @Override public Iterator<OatDexFile> iterator() { - return new Iterator<OatDexFile>() { - int index = 0; - int offset = oatHeader.getDexListStart(); - - @Override public boolean hasNext() { - return index < size(); + return Iterators.transform(new DexEntryIterator(), new Function<DexEntry, OatDexFile>() { + @Nullable @Override public OatDexFile apply(DexEntry dexEntry) { + return dexEntry.getDexFile(); } + }); + } + }; + } - @Override public OatDexFile next() { - int filenameLength = readSmallUint(offset); - offset += 4; - - // TODO: what is the correct character encoding? - String filename = new String(buf, offset, filenameLength, Charset.forName("US-ASCII")); - offset += filenameLength; - - offset += 4; // checksum - - int dexOffset = readSmallUint(offset) + oatHeader.offset; - offset += 4; - - int classCount = readSmallUint(dexOffset + HeaderItem.CLASS_COUNT_OFFSET); - offset += 4 * classCount; - - index++; - - return new OatDexFile(dexOffset, filename); - } + @Nonnull @Override public List<String> getDexEntryNames() throws IOException { + return new AbstractForwardSequentialList<String>() { + @Override public int size() { + return oatHeader.getDexFileCount(); + } - @Override public void remove() { - throw new UnsupportedOperationException(); + @Nonnull @Override public Iterator<String> iterator() { + return Iterators.transform(new DexEntryIterator(), new Function<DexEntry, String>() { + @Nullable @Override public String apply(DexEntry dexEntry) { + return dexEntry.entryName; } - }; + }); } }; } - public class OatDexFile extends DexBackedDexFile { + @Nullable @Override public OatDexFile getEntry(@Nonnull String entryName) throws IOException { + DexEntryIterator iterator = new DexEntryIterator(); + while (iterator.hasNext()) { + DexEntry entry = iterator.next(); + + if (entry.entryName.equals(entryName)) { + return entry.getDexFile(); + } + } + return null; + } + + public class OatDexFile extends DexBackedDexFile implements MultiDexContainer.MultiDexFile { @Nonnull public final String filename; - public OatDexFile(int offset, @Nonnull String filename) { - super(opcodes, OatFile.this.buf, offset); + public OatDexFile(byte[] buf, int offset, @Nonnull String filename) { + super(opcodes, buf, offset); this.filename = filename; } - public int getOatVersion() { - return OatFile.this.getOatVersion(); + @Nonnull @Override public String getEntryName() { + return filename; + } + + @Nonnull @Override public OatFile getContainer() { + return OatFile.this; } @Override public boolean hasOdexOpcodes() { @@ -211,57 +248,87 @@ public class OatFile extends BaseDexBuffer { } private class OatHeader { - private final int offset; + private final int headerOffset; public OatHeader(int offset) { - this.offset = offset; + this.headerOffset = offset; } public boolean isValid() { for (int i=0; i<OAT_MAGIC.length; i++) { - if (buf[offset + i] != OAT_MAGIC[i]) { + if (buf[headerOffset + i] != OAT_MAGIC[i]) { return false; } } for (int i=4; i<7; i++) { - if (buf[offset + i] < '0' || buf[offset + i] > '9') { + if (buf[headerOffset + i] < '0' || buf[headerOffset + i] > '9') { return false; } } - return buf[offset + 7] == 0; + return buf[headerOffset + 7] == 0; } public int getVersion() { - return Integer.valueOf(new String(buf, offset + 4, 3)); + return Integer.valueOf(new String(buf, headerOffset + 4, 3)); } public int getDexFileCount() { - return readSmallUint(offset + 20); + return readSmallUint(headerOffset + 20); } public int getKeyValueStoreSize() { - int version = getVersion(); - if (version < 56) { + if (getVersion() < MIN_OAT_VERSION) { throw new IllegalStateException("Unsupported oat version"); } int fieldOffset = 17 * 4; - return readSmallUint(offset + fieldOffset); + return readSmallUint(headerOffset + fieldOffset); } public int getHeaderSize() { - int version = getVersion(); - if (version >= 56) { - return 18*4 + getKeyValueStoreSize(); - } else { + if (getVersion() < MIN_OAT_VERSION) { throw new IllegalStateException("Unsupported oat version"); } + return 18*4 + getKeyValueStoreSize(); + } + + @Nullable + public String getKeyValue(@Nonnull String key) { + int size = getKeyValueStoreSize(); + int offset = headerOffset + 18 * 4; + int endOffset = offset + size; + + while (offset < endOffset) { + int keyStartOffset = offset; + while (offset < endOffset && buf[offset] != '\0') { + offset++; + } + if (offset >= endOffset) { + throw new InvalidOatFileException("Oat file contains truncated key value store"); + } + int keyEndOffset = offset; + + String k = new String(buf, keyStartOffset, keyEndOffset - keyStartOffset); + if (k.equals(key)) { + int valueStartOffset = ++offset; + while (offset < endOffset && buf[offset] != '\0') { + offset++; + } + if (offset >= endOffset) { + throw new InvalidOatFileException("Oat file contains truncated key value store"); + } + int valueEndOffset = offset; + return new String(buf, valueStartOffset, valueEndOffset - valueStartOffset); + } + offset++; + } + return null; } public int getDexListStart() { - return offset + getHeaderSize(); + return headerOffset + getHeaderSize(); } } @@ -481,7 +548,74 @@ public class OatFile extends BaseDexBuffer { return new String(buf, start, end-start, Charset.forName("US-ASCII")); } + } + + private class DexEntry { + public final String entryName; + public final byte[] buf; + public final int dexOffset; + + + public DexEntry(String entryName, byte[] buf, int dexOffset) { + this.entryName = entryName; + this.buf = buf; + this.dexOffset = dexOffset; + } + + public OatDexFile getDexFile() { + return new OatDexFile(buf, dexOffset, entryName); + } + } + + private class DexEntryIterator implements Iterator<DexEntry> { + int index = 0; + int offset = oatHeader.getDexListStart(); + + @Override public boolean hasNext() { + return index < oatHeader.getDexFileCount(); + } + + @Override public DexEntry next() { + int filenameLength = readSmallUint(offset); + offset += 4; + + // TODO: what is the correct character encoding? + String filename = new String(buf, offset, filenameLength, Charset.forName("US-ASCII")); + offset += filenameLength; + + offset += 4; // checksum + + int dexOffset = readSmallUint(offset); + offset += 4; + + byte[] buf; + if (getOatVersion() >= 87 && vdexProvider != null && vdexProvider.getVdex() != null) { + buf = vdexProvider.getVdex(); + } else { + buf = OatFile.this.buf; + dexOffset += oatHeader.headerOffset; + } + + if (getOatVersion() >= 75) { + offset += 4; // offset to class offsets table + } + if (getOatVersion() >= 73) { + offset += 4; // lookup table offset + } + if (getOatVersion() < 75) { + // prior to 75, the class offsets are included here directly + int classCount = readSmallUint(dexOffset + HeaderItem.CLASS_COUNT_OFFSET); + offset += 4 * classCount; + } + + index++; + + return new DexEntry(filename, buf, dexOffset); + } + @Override public void remove() { + throw new UnsupportedOperationException(); + } } public static class InvalidOatFileException extends RuntimeException { @@ -493,4 +627,9 @@ public class OatFile extends BaseDexBuffer { public static class NotAnOatFileException extends RuntimeException { public NotAnOatFileException() {} } + + public interface VdexProvider { + @Nullable + byte[] getVdex(); + } }
\ No newline at end of file diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java new file mode 100644 index 00000000..50052c20 --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java @@ -0,0 +1,201 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2.dexbacked; + +import com.google.common.collect.Lists; +import com.google.common.io.ByteStreams; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; +import org.jf.dexlib2.dexbacked.ZipDexContainer.ZipDexFile; +import org.jf.dexlib2.iface.MultiDexContainer; +import org.jf.dexlib2.util.DexUtil; +import org.jf.dexlib2.util.DexUtil.InvalidFile; +import org.jf.dexlib2.util.DexUtil.UnsupportedFile; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Represents a zip file that contains dex files (i.e. an apk or jar file) + */ +public class ZipDexContainer implements MultiDexContainer<ZipDexFile> { + + private final File zipFilePath; + private final Opcodes opcodes; + + /** + * Constructs a new ZipDexContainer for the given zip file + * + * @param zipFilePath The path to the zip file + * @param opcodes The Opcodes instance to use when loading dex files from this container + */ + public ZipDexContainer(@Nonnull File zipFilePath, @Nonnull Opcodes opcodes) { + this.zipFilePath = zipFilePath; + this.opcodes = opcodes; + } + + @Nonnull @Override public Opcodes getOpcodes() { + return opcodes; + } + + /** + * Gets a list of the names of dex files in this zip file. + * + * @return A list of the names of dex files in this zip file + */ + @Nonnull @Override public List<String> getDexEntryNames() throws IOException { + List<String> entryNames = Lists.newArrayList(); + ZipFile zipFile = getZipFile(); + try { + Enumeration<? extends ZipEntry> entriesEnumeration = zipFile.entries(); + + while (entriesEnumeration.hasMoreElements()) { + ZipEntry entry = entriesEnumeration.nextElement(); + + if (!isDex(zipFile, entry)) { + continue; + } + + entryNames.add(entry.getName()); + } + + return entryNames; + } finally { + zipFile.close(); + } + } + + /** + * Loads a dex file from a specific named entry. + * + * @param entryName The name of the entry + * @return A ZipDexFile, or null if there is no entry with the given name + * @throws NotADexFile If the entry isn't a dex file + */ + @Nullable @Override public ZipDexFile getEntry(@Nonnull String entryName) throws IOException { + ZipFile zipFile = getZipFile(); + try { + ZipEntry entry = zipFile.getEntry(entryName); + if (entry == null) { + return null; + } + + return loadEntry(zipFile, entry); + } finally { + zipFile.close(); + } + } + + public boolean isZipFile() { + ZipFile zipFile = null; + try { + zipFile = getZipFile(); + return true; + } catch (IOException ex) { + return false; + } catch (NotAZipFileException ex) { + return false; + } finally { + if(zipFile != null) { + try { + zipFile.close(); + } catch (IOException ex) { + // just eat it + } + } + } + } + + public class ZipDexFile extends DexBackedDexFile implements MultiDexContainer.MultiDexFile { + + private final String entryName; + + protected ZipDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, @Nonnull String entryName) { + super(opcodes, buf, 0); + this.entryName = entryName; + } + + @Nonnull @Override public String getEntryName() { + return entryName; + } + + @Nonnull @Override public MultiDexContainer getContainer() { + return ZipDexContainer.this; + } + } + + protected boolean isDex(@Nonnull ZipFile zipFile, @Nonnull ZipEntry zipEntry) throws IOException { + InputStream inputStream = new BufferedInputStream(zipFile.getInputStream(zipEntry)); + try { + DexUtil.verifyDexHeader(inputStream); + } catch (NotADexFile ex) { + return false; + } catch (InvalidFile ex) { + return false; + } catch (UnsupportedFile ex) { + return false; + } finally { + inputStream.close(); + } + return true; + } + + protected ZipFile getZipFile() throws IOException { + try { + return new ZipFile(zipFilePath); + } catch (IOException ex) { + throw new NotAZipFileException(); + } + } + + @Nonnull + protected ZipDexFile loadEntry(@Nonnull ZipFile zipFile, @Nonnull ZipEntry zipEntry) throws IOException { + InputStream inputStream = zipFile.getInputStream(zipEntry); + try { + byte[] buf = ByteStreams.toByteArray(inputStream); + return new ZipDexFile(opcodes, buf, zipEntry.getName()); + } finally { + inputStream.close(); + } + } + + public static class NotAZipFileException extends RuntimeException { + } +} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/HeaderItem.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/HeaderItem.java index 2f3af4c3..e8db9698 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/HeaderItem.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/HeaderItem.java @@ -42,15 +42,8 @@ import javax.annotation.Nullable; public class HeaderItem { public static final int ITEM_SIZE = 0x70; - /** - * The magic numbers for dex files. - * - * They are: "dex\n035\0", "dex\n037\0", and "dex\n038\0". - */ - public static final byte[][] MAGIC_VALUES= new byte[][] { - new byte[]{0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00}, - new byte[]{0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x37, 0x00}, - new byte[]{0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x38, 0x00}}; + private static final byte[] MAGIC_VALUE = new byte[] { 0x64, 0x65, 0x78, 0x0a, 0x00, 0x00, 0x00, 0x00 }; + private static final int[] SUPPORTED_DEX_VERSIONS = new int[] { 35, 37, 38 }; public static final int LITTLE_ENDIAN_TAG = 0x12345678; public static final int BIG_ENDIAN_TAG = 0x78563412; @@ -231,51 +224,100 @@ public class HeaderItem { return "Invalid"; } - /** - * Get the higest magic number supported by Android for this api level. + * Get the highest magic number supported by Android for this api level. * @return The dex file magic number */ public static byte[] getMagicForApi(int api) { if (api < 24) { // Prior to Android N we only support dex version 035. - return HeaderItem.MAGIC_VALUES[0]; + return getMagicForDexVersion(35); } else if (api < 26) { // On android N and later we support dex version 037. - return HeaderItem.MAGIC_VALUES[1]; + return getMagicForDexVersion(37); } else { // On android O and later we support dex version 038. - return HeaderItem.MAGIC_VALUES[2]; + return getMagicForDexVersion(38); } } - private static int getVersion(byte[] buf, int offset) { + public static byte[] getMagicForDexVersion(int dexVersion) { + byte[] magic = MAGIC_VALUE.clone(); + + if (dexVersion < 0 || dexVersion > 999) { + throw new IllegalArgumentException("dexVersion must be within [0, 999]"); + } + + for (int i=6; i>=4; i--) { + int digit = dexVersion % 10; + magic[i] = (byte)('0' + digit); + dexVersion /= 10; + } + + return magic; + } + + /** + * Verifies the magic value at the beginning of a dex file + * + * @param buf A byte array containing at least the first 8 bytes of a dex file + * @param offset The offset within the buffer to the beginning of the dex header + * @return True if the magic value is valid + */ + public static boolean verifyMagic(byte[] buf, int offset) { if (buf.length - offset < 8) { - return 0; + return false; } - boolean matches = true; - for (int i=0; i<MAGIC_VALUES.length; i++) { - byte[] expected = MAGIC_VALUES[i]; - matches = true; - for (int j=0; j<8; j++) { - if (buf[offset + j] != expected[j]) { - matches = false; - break; + for (int i=0; i<4; i++) { + if (buf[offset + i] != MAGIC_VALUE[i]) { + return false; } } - if (matches) { - return i==0?35:(i==1?37:38); + for (int i=4; i<7; i++) { + if (buf[offset + i] < '0' || + buf[offset + i] > '9') { + return false; } } - return 0; + if (buf[offset + 7] != MAGIC_VALUE[7]) { + return false; } - public static boolean verifyMagic(byte[] buf, int offset) { - // verifies the magic value - return getVersion(buf, offset) != 0; + return true; + } + + /** + * Gets the dex version from a dex header + * + * @param buf A byte array containing at least the first 7 bytes of a dex file + * @param offset The offset within the buffer to the beginning of the dex header + * @return The dex version if the header is valid or -1 if the header is invalid + */ + public static int getVersion(byte[] buf, int offset) { + if (!verifyMagic(buf, offset)) { + return -1; + } + + return getVersionUnchecked(buf, offset); + } + + private static int getVersionUnchecked(byte[] buf, int offset) { + int version = (buf[offset + 4] - '0') * 100; + version += (buf[offset + 5] - '0') * 10; + version += buf[offset + 6] - '0'; + + return version; } + public static boolean isSupportedDexVersion(int version) { + for (int i=0; i<SUPPORTED_DEX_VERSIONS.length; i++) { + if (SUPPORTED_DEX_VERSIONS[i] == version) { + return true; + } + } + return false; + } public static int getEndian(byte[] buf, int offset) { BaseDexBuffer bdb = new BaseDexBuffer(buf); diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/OdexHeaderItem.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/OdexHeaderItem.java index c6599bc4..7566834a 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/OdexHeaderItem.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/OdexHeaderItem.java @@ -36,10 +36,8 @@ import org.jf.dexlib2.dexbacked.BaseDexBuffer; public class OdexHeaderItem { public static final int ITEM_SIZE = 40; - public static final byte[][] MAGIC_VALUES= new byte[][] { - new byte[] {0x64, 0x65, 0x79, 0x0A, 0x30, 0x33, 0x35, 0x00}, // "dey\n035\0" - new byte[] {0x64, 0x65, 0x79, 0x0A, 0x30, 0x33, 0x36, 0x00} // "dey\n036\0" - }; + private static final byte[] MAGIC_VALUE = new byte[] { 0x64, 0x65, 0x79, 0x0A, 0x00, 0x00, 0x00, 0x00 }; + private static final int[] SUPPORTED_ODEX_VERSIONS = new int[] { 35, 36 }; public static final int MAGIC_OFFSET = 0; public static final int MAGIC_LENGTH = 8; @@ -51,31 +49,66 @@ public class OdexHeaderItem { public static final int AUX_LENGTH_OFFSET = 28; public static final int FLAGS_OFFSET = 32; - public static int getVersion(byte[] magic) { - if (magic.length < 8) { - return 0; + /** + * Verifies the magic value at the beginning of an odex file + * + * @param buf A byte array containing at least the first 8 bytes of an odex file + * @param offset The offset within the buffer to the beginning of the odex header + * @return True if the magic value is valid + */ + public static boolean verifyMagic(byte[] buf, int offset) { + if (buf.length - offset < 8) { + return false; } - boolean matches = true; - for (int i=0; i<MAGIC_VALUES.length; i++) { - byte[] expected = MAGIC_VALUES[i]; - matches = true; - for (int j=0; j<8; j++) { - if (magic[j] != expected[j]) { - matches = false; - break; - } + for (int i=0; i<4; i++) { + if (buf[offset + i] != MAGIC_VALUE[i]) { + return false; } - if (matches) { - return i==0?35:36; + } + for (int i=4; i<7; i++) { + if (buf[offset + i] < '0' || + buf[offset + i] > '9') { + return false; } } - return 0; + if (buf[offset + 7] != MAGIC_VALUE[7]) { + return false; + } + + return true; + } + + /** + * Gets the dex version from an odex header + * + * @param buf A byte array containing at least the first 7 bytes of an odex file + * @param offset The offset within the buffer to the beginning of the odex header + * @return The odex version if the header is valid or -1 if the header is invalid + */ + public static int getVersion(byte[] buf, int offset) { + if (!verifyMagic(buf, offset)) { + return -1; + } + + return getVersionUnchecked(buf, offset); + } + + private static int getVersionUnchecked(byte[] buf, int offset) { + int version = (buf[offset + 4] - '0') * 100; + version += (buf[offset + 5] - '0') * 10; + version += buf[offset + 6] - '0'; + + return version; } - public static boolean verifyMagic(byte[] buf) { - // verifies the magic value - return getVersion(buf) != 0; + public static boolean isSupportedOdexVersion(int version) { + for (int i=0; i<SUPPORTED_ODEX_VERSIONS.length; i++) { + if (SUPPORTED_ODEX_VERSIONS[i] == version) { + return true; + } + } + return false; } public static int getDexOffset(byte[] buf) { diff --git a/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java b/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java new file mode 100644 index 00000000..6c4e769a --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2.iface; + +import org.jf.dexlib2.Opcodes; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.List; + +/** + * This class represents a dex container that can contain multiple, named dex files + */ +public interface MultiDexContainer<T extends DexFile> { + /** + * @return A list of the names of dex entries in this container + */ + @Nonnull List<String> getDexEntryNames() throws IOException; + + /** + * Gets the dex entry with the given name + * + * @param entryName The name of the entry + * @return A DexFile, or null if no entry with that name is found + */ + @Nullable T getEntry(@Nonnull String entryName) throws IOException; + + /** + * @return the Opcodes instance associated with this MultiDexContainer + */ + @Nonnull Opcodes getOpcodes(); + + /** + * This class represents a dex file that is contained in a MultiDexContainer + */ + interface MultiDexFile extends DexFile { + /** + * @return The name of this entry within its container + */ + @Nonnull String getEntryName(); + + /** + * @return The MultiDexContainer that contains this dex file + */ + @Nonnull MultiDexContainer<? extends MultiDexFile> getContainer(); + } +} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/immutable/ImmutableDexFile.java b/dexlib2/src/main/java/org/jf/dexlib2/immutable/ImmutableDexFile.java index 2112bd07..76f39a14 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/immutable/ImmutableDexFile.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/immutable/ImmutableDexFile.java @@ -45,18 +45,6 @@ public class ImmutableDexFile implements DexFile { @Nonnull protected final ImmutableSet<? extends ImmutableClassDef> classes; @Nonnull private final Opcodes opcodes; - @Deprecated - public ImmutableDexFile(@Nullable Collection<? extends ClassDef> classes) { - this.classes = ImmutableClassDef.immutableSetOf(classes); - this.opcodes = Opcodes.forApi(19); - } - - @Deprecated - public ImmutableDexFile(@Nullable ImmutableSet<? extends ImmutableClassDef> classes) { - this.classes = ImmutableUtils.nullToEmptySet(classes); - this.opcodes = Opcodes.forApi(19); - } - public ImmutableDexFile(@Nonnull Opcodes opcodes, @Nullable Collection<? extends ClassDef> classes) { this.classes = ImmutableClassDef.immutableSetOf(classes); this.opcodes = opcodes; diff --git a/dexlib2/src/main/java/org/jf/dexlib2/util/DexUtil.java b/dexlib2/src/main/java/org/jf/dexlib2/util/DexUtil.java new file mode 100644 index 00000000..389edfd7 --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/util/DexUtil.java @@ -0,0 +1,189 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2.util; + +import com.google.common.io.ByteStreams; +import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; +import org.jf.dexlib2.dexbacked.DexBackedOdexFile.NotAnOdexFile; +import org.jf.dexlib2.dexbacked.raw.HeaderItem; +import org.jf.dexlib2.dexbacked.raw.OdexHeaderItem; + +import javax.annotation.Nonnull; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +public class DexUtil { + + /** + * Reads in the dex header from the given input stream and verifies that it is valid and a supported version + * + * The inputStream must support mark(), and will be reset to initial position upon exiting the method + * + * @param inputStream An input stream that is positioned at a dex header + * @throws NotADexFile If the file is not a dex file + * @throws InvalidFile If the header appears to be a dex file, but is not valid for some reason + * @throws UnsupportedFile If the dex header is valid, but uses unsupported functionality + */ + public static void verifyDexHeader(@Nonnull InputStream inputStream) throws IOException { + if (!inputStream.markSupported()) { + throw new IllegalArgumentException("InputStream must support mark"); + } + inputStream.mark(44); + byte[] partialHeader = new byte[44]; + try { + ByteStreams.readFully(inputStream, partialHeader); + } catch (EOFException ex) { + throw new NotADexFile("File is too short"); + } finally { + inputStream.reset(); + } + + verifyDexHeader(partialHeader, 0); + } + + /** + * Verifies that the dex header is valid and a supported version + * + * @param buf A byte array containing at least the first 44 bytes of a dex file + * @param offset The offset within the array to the dex header + * @throws NotADexFile If the file is not a dex file + * @throws InvalidFile If the header appears to be a dex file, but is not valid for some reason + * @throws UnsupportedFile If the dex header is valid, but uses unsupported functionality + */ + public static void verifyDexHeader(@Nonnull byte[] buf, int offset) { + int dexVersion = HeaderItem.getVersion(buf, offset); + if (dexVersion == -1) { + StringBuilder sb = new StringBuilder("Not a valid dex magic value:"); + for (int i=0; i<8; i++) { + sb.append(String.format(" %02x", buf[i])); + } + throw new NotADexFile(sb.toString()); + } + + if (!HeaderItem.isSupportedDexVersion(dexVersion)) { + throw new UnsupportedFile(String.format("Dex version %03d is not supported", dexVersion)); + } + + int endian = HeaderItem.getEndian(buf, offset); + if (endian == HeaderItem.BIG_ENDIAN_TAG) { + throw new UnsupportedFile("Big endian dex files are not supported"); + } + + if (endian != HeaderItem.LITTLE_ENDIAN_TAG) { + throw new InvalidFile(String.format("Invalid endian tag: 0x%x", endian)); + } + } + + /** + * Reads in the odex header from the given input stream and verifies that it is valid and a supported version + * + * The inputStream must support mark(), and will be reset to initial position upon exiting the method + * + * @param inputStream An input stream that is positioned at an odex header + * @throws NotAnOdexFile If the file is not an odex file + * @throws UnsupportedFile If the odex header is valid, but is an unsupported version + */ + public static void verifyOdexHeader(@Nonnull InputStream inputStream) throws IOException { + if (!inputStream.markSupported()) { + throw new IllegalArgumentException("InputStream must support mark"); + } + inputStream.mark(8); + byte[] partialHeader = new byte[8]; + try { + ByteStreams.readFully(inputStream, partialHeader); + } catch (EOFException ex) { + throw new NotAnOdexFile("File is too short"); + } finally { + inputStream.reset(); + } + + verifyOdexHeader(partialHeader, 0); + } + + /** + * Verifies that the odex header is valid and a supported version + * + * @param buf A byte array containing at least the first 8 bytes of an odex file + * @param offset The offset within the array to the odex header + * @throws NotAnOdexFile If the file is not an odex file + * @throws UnsupportedFile If the odex header is valid, but uses unsupported functionality + */ + public static void verifyOdexHeader(@Nonnull byte[] buf, int offset) { + int odexVersion = OdexHeaderItem.getVersion(buf, offset); + if (odexVersion == -1) { + StringBuilder sb = new StringBuilder("Not a valid odex magic value:"); + for (int i=0; i<8; i++) { + sb.append(String.format(" %02x", buf[i])); + } + throw new NotAnOdexFile(sb.toString()); + } + + if (!OdexHeaderItem.isSupportedOdexVersion(odexVersion)) { + throw new UnsupportedFile(String.format("Odex version %03d is not supported", odexVersion)); + } + } + + public static class InvalidFile extends RuntimeException { + public InvalidFile() { + } + + public InvalidFile(String message) { + super(message); + } + + public InvalidFile(String message, Throwable cause) { + super(message, cause); + } + + public InvalidFile(Throwable cause) { + super(cause); + } + } + + public static class UnsupportedFile extends RuntimeException { + public UnsupportedFile() { + } + + public UnsupportedFile(String message) { + super(message); + } + + public UnsupportedFile(String message, Throwable cause) { + super(message, cause); + } + + public UnsupportedFile(Throwable cause) { + super(cause); + } + } +} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java index 6ca1ce93..00cce65d 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java @@ -53,11 +53,7 @@ import org.jf.dexlib2.iface.instruction.Instruction; import org.jf.dexlib2.iface.instruction.OneRegisterInstruction; import org.jf.dexlib2.iface.instruction.ReferenceInstruction; import org.jf.dexlib2.iface.instruction.formats.*; -import org.jf.dexlib2.iface.reference.FieldReference; -import org.jf.dexlib2.iface.reference.MethodProtoReference; -import org.jf.dexlib2.iface.reference.MethodReference; -import org.jf.dexlib2.iface.reference.StringReference; -import org.jf.dexlib2.iface.reference.TypeReference; +import org.jf.dexlib2.iface.reference.*; import org.jf.dexlib2.util.InstructionUtil; import org.jf.dexlib2.util.MethodUtil; import org.jf.dexlib2.util.ReferenceUtil; @@ -92,7 +88,18 @@ public abstract class DexWriter< TypeListKey, FieldKey, MethodKey, EncodedValue, - AnnotationElement extends org.jf.dexlib2.iface.AnnotationElement> { + AnnotationElement extends org.jf.dexlib2.iface.AnnotationElement, + StringSectionType extends StringSection<StringKey, StringRef>, + TypeSectionType extends TypeSection<StringKey, TypeKey, TypeRef>, + ProtoSectionType extends ProtoSection<StringKey, TypeKey, ProtoRefKey, TypeListKey>, + FieldSectionType extends FieldSection<StringKey, TypeKey, FieldRefKey, FieldKey>, + MethodSectionType extends MethodSection<StringKey, TypeKey, ProtoRefKey, MethodRefKey, MethodKey>, + ClassSectionType extends ClassSection<StringKey, TypeKey, TypeListKey, ClassKey, FieldKey, MethodKey, + AnnotationSetKey, EncodedValue>, + TypeListSectionType extends TypeListSection<TypeKey, TypeListKey>, + AnnotationSectionType extends AnnotationSection<StringKey, TypeKey, AnnotationKey, AnnotationElement, + EncodedValue>, + AnnotationSetSectionType extends AnnotationSetSection<AnnotationKey, AnnotationSetKey>> { public static final int NO_INDEX = -1; public static final int NO_OFFSET = 0; @@ -124,43 +131,34 @@ public abstract class DexWriter< protected int numCodeItemItems = 0; protected int numClassDataItems = 0; - protected final StringSection<StringKey, StringRef> stringSection; - protected final TypeSection<StringKey, TypeKey, TypeRef> typeSection; - protected final ProtoSection<StringKey, TypeKey, ProtoRefKey, TypeListKey> protoSection; - protected final FieldSection<StringKey, TypeKey, FieldRefKey, FieldKey> fieldSection; - protected final MethodSection<StringKey, TypeKey, ProtoRefKey, MethodRefKey, MethodKey> methodSection; - protected final ClassSection<StringKey, TypeKey, TypeListKey, ClassKey, FieldKey, MethodKey, AnnotationSetKey, - EncodedValue> classSection; + public final StringSectionType stringSection; + public final TypeSectionType typeSection; + public final ProtoSectionType protoSection; + public final FieldSectionType fieldSection; + public final MethodSectionType methodSection; + public final ClassSectionType classSection; - protected final TypeListSection<TypeKey, TypeListKey> typeListSection; - protected final AnnotationSection<StringKey, TypeKey, AnnotationKey, AnnotationElement, EncodedValue> annotationSection; - protected final AnnotationSetSection<AnnotationKey, AnnotationSetKey> annotationSetSection; - - protected DexWriter(Opcodes opcodes, - StringSection<StringKey, StringRef> stringSection, - TypeSection<StringKey, TypeKey, TypeRef> typeSection, - ProtoSection<StringKey, TypeKey, ProtoRefKey, TypeListKey> protoSection, - FieldSection<StringKey, TypeKey, FieldRefKey, FieldKey> fieldSection, - MethodSection<StringKey, TypeKey, ProtoRefKey, MethodRefKey, MethodKey> methodSection, - ClassSection<StringKey, TypeKey, TypeListKey, ClassKey, FieldKey, MethodKey, AnnotationSetKey, - EncodedValue> classSection, - TypeListSection<TypeKey, TypeListKey> typeListSection, - AnnotationSection<StringKey, TypeKey, AnnotationKey, AnnotationElement, - EncodedValue> annotationSection, - AnnotationSetSection<AnnotationKey, AnnotationSetKey> annotationSetSection) { + public final TypeListSectionType typeListSection; + public final AnnotationSectionType annotationSection; + public final AnnotationSetSectionType annotationSetSection; + + protected DexWriter(Opcodes opcodes) { this.opcodes = opcodes; - this.stringSection = stringSection; - this.typeSection = typeSection; - this.protoSection = protoSection; - this.fieldSection = fieldSection; - this.methodSection = methodSection; - this.classSection = classSection; - this.typeListSection = typeListSection; - this.annotationSection = annotationSection; - this.annotationSetSection = annotationSetSection; + SectionProvider sectionProvider = getSectionProvider(); + this.stringSection = sectionProvider.getStringSection(); + this.typeSection = sectionProvider.getTypeSection(); + this.protoSection = sectionProvider.getProtoSection(); + this.fieldSection = sectionProvider.getFieldSection(); + this.methodSection = sectionProvider.getMethodSection(); + this.classSection = sectionProvider.getClassSection(); + this.typeListSection = sectionProvider.getTypeListSection(); + this.annotationSection = sectionProvider.getAnnotationSection(); + this.annotationSetSection = sectionProvider.getAnnotationSetSection(); } + @Nonnull protected abstract SectionProvider getSectionProvider(); + protected abstract void writeEncodedValue(@Nonnull InternalEncodedValueWriter writer, @Nonnull EncodedValue encodedValue) throws IOException; @@ -192,12 +190,12 @@ public abstract class DexWriter< private int getDataSectionOffset() { return HeaderItem.ITEM_SIZE + - stringSection.getItems().size() * StringIdItem.ITEM_SIZE + - typeSection.getItems().size() * TypeIdItem.ITEM_SIZE + - protoSection.getItems().size() * ProtoIdItem.ITEM_SIZE + - fieldSection.getItems().size() * FieldIdItem.ITEM_SIZE + - methodSection.getItems().size() * MethodIdItem.ITEM_SIZE + - classSection.getItems().size() * ClassDefItem.ITEM_SIZE; + stringSection.getItemCount() * StringIdItem.ITEM_SIZE + + typeSection.getItemCount() * TypeIdItem.ITEM_SIZE + + protoSection.getItemCount() * ProtoIdItem.ITEM_SIZE + + fieldSection.getItemCount() * FieldIdItem.ITEM_SIZE + + methodSection.getItemCount() * MethodIdItem.ITEM_SIZE + + classSection.getItemCount() * ClassDefItem.ITEM_SIZE; } @Nonnull @@ -227,6 +225,22 @@ public abstract class DexWriter< return classReferences; } + /** + * Checks whether any of the size-sensitive constant pools have overflowed. + * + * This checks whether the type, method, field pools are larger than 64k entries. + * + * Note that even if this returns true, it may still be possible to successfully write the dex file, if the + * overflowed items are not referenced anywhere that uses a 16-bit index + * + * @return true if any of the size-sensitive constant pools have overflowed + */ + public boolean hasOverflowed() { + return methodSection.getItemCount() > (1 << 16) || + typeSection.getItemCount() > (1 << 16) || + fieldSection.getItemCount() > (1 << 16); + } + public void writeTo(@Nonnull DexDataStore dest) throws IOException { this.writeTo(dest, MemoryDeferredOutputStream.getFactory()); } @@ -801,7 +815,14 @@ public abstract class DexWriter< int debugItemOffset = writeDebugItem(offsetWriter, debugWriter, classSection.getParameterNames(methodKey), debugItems); - int codeItemOffset = writeCodeItem(codeWriter, ehBuf, methodKey, tryBlocks, instructions, debugItemOffset); + int codeItemOffset; + try { + codeItemOffset = writeCodeItem( + codeWriter, ehBuf, methodKey, tryBlocks, instructions, debugItemOffset); + } catch (RuntimeException ex) { + throw new ExceptionWithContext(ex, "Exception occurred while writing code_item for method %s", + methodSection.getMethodReference(methodKey)); + } if (codeItemOffset != -1) { codeOffsets.add(new CodeItemOffset<MethodKey>(methodKey, codeItemOffset)); @@ -950,105 +971,111 @@ public abstract class DexWriter< methodSection, protoSection); writer.writeInt(codeUnitCount); + int codeOffset = 0; for (Instruction instruction: instructions) { - switch (instruction.getOpcode().format) { - case Format10t: - instructionWriter.write((Instruction10t)instruction); - break; - case Format10x: - instructionWriter.write((Instruction10x)instruction); - break; - case Format11n: - instructionWriter.write((Instruction11n)instruction); - break; - case Format11x: - instructionWriter.write((Instruction11x)instruction); - break; - case Format12x: - instructionWriter.write((Instruction12x)instruction); - break; - case Format20bc: - instructionWriter.write((Instruction20bc)instruction); - break; - case Format20t: - instructionWriter.write((Instruction20t)instruction); - break; - case Format21c: - instructionWriter.write((Instruction21c)instruction); - break; - case Format21ih: - instructionWriter.write((Instruction21ih)instruction); - break; - case Format21lh: - instructionWriter.write((Instruction21lh)instruction); - break; - case Format21s: - instructionWriter.write((Instruction21s)instruction); - break; - case Format21t: - instructionWriter.write((Instruction21t)instruction); - break; - case Format22b: - instructionWriter.write((Instruction22b)instruction); - break; - case Format22c: - instructionWriter.write((Instruction22c)instruction); - break; - case Format22s: - instructionWriter.write((Instruction22s)instruction); - break; - case Format22t: - instructionWriter.write((Instruction22t)instruction); - break; - case Format22x: - instructionWriter.write((Instruction22x)instruction); - break; - case Format23x: - instructionWriter.write((Instruction23x)instruction); - break; - case Format30t: - instructionWriter.write((Instruction30t)instruction); - break; - case Format31c: - instructionWriter.write((Instruction31c)instruction); - break; - case Format31i: - instructionWriter.write((Instruction31i)instruction); - break; - case Format31t: - instructionWriter.write((Instruction31t)instruction); - break; - case Format32x: - instructionWriter.write((Instruction32x)instruction); - break; - case Format35c: - instructionWriter.write((Instruction35c)instruction); - break; - case Format3rc: - instructionWriter.write((Instruction3rc)instruction); - break; - case Format45cc: - instructionWriter.write((Instruction45cc) instruction); - break; - case Format4rcc: - instructionWriter.write((Instruction4rcc) instruction); - break; - case Format51l: - instructionWriter.write((Instruction51l)instruction); - break; - case ArrayPayload: - instructionWriter.write((ArrayPayload)instruction); - break; - case PackedSwitchPayload: - instructionWriter.write((PackedSwitchPayload)instruction); - break; - case SparseSwitchPayload: - instructionWriter.write((SparseSwitchPayload)instruction); - break; - default: - throw new ExceptionWithContext("Unsupported instruction format: %s", - instruction.getOpcode().format); + try { + switch (instruction.getOpcode().format) { + case Format10t: + instructionWriter.write((Instruction10t)instruction); + break; + case Format10x: + instructionWriter.write((Instruction10x)instruction); + break; + case Format11n: + instructionWriter.write((Instruction11n)instruction); + break; + case Format11x: + instructionWriter.write((Instruction11x)instruction); + break; + case Format12x: + instructionWriter.write((Instruction12x)instruction); + break; + case Format20bc: + instructionWriter.write((Instruction20bc)instruction); + break; + case Format20t: + instructionWriter.write((Instruction20t)instruction); + break; + case Format21c: + instructionWriter.write((Instruction21c)instruction); + break; + case Format21ih: + instructionWriter.write((Instruction21ih)instruction); + break; + case Format21lh: + instructionWriter.write((Instruction21lh)instruction); + break; + case Format21s: + instructionWriter.write((Instruction21s)instruction); + break; + case Format21t: + instructionWriter.write((Instruction21t)instruction); + break; + case Format22b: + instructionWriter.write((Instruction22b)instruction); + break; + case Format22c: + instructionWriter.write((Instruction22c)instruction); + break; + case Format22s: + instructionWriter.write((Instruction22s)instruction); + break; + case Format22t: + instructionWriter.write((Instruction22t)instruction); + break; + case Format22x: + instructionWriter.write((Instruction22x)instruction); + break; + case Format23x: + instructionWriter.write((Instruction23x)instruction); + break; + case Format30t: + instructionWriter.write((Instruction30t)instruction); + break; + case Format31c: + instructionWriter.write((Instruction31c)instruction); + break; + case Format31i: + instructionWriter.write((Instruction31i)instruction); + break; + case Format31t: + instructionWriter.write((Instruction31t)instruction); + break; + case Format32x: + instructionWriter.write((Instruction32x)instruction); + break; + case Format35c: + instructionWriter.write((Instruction35c)instruction); + break; + case Format3rc: + instructionWriter.write((Instruction3rc)instruction); + break; + case Format45cc: + instructionWriter.write((Instruction45cc)instruction); + break; + case Format4rcc: + instructionWriter.write((Instruction4rcc)instruction); + break; + case Format51l: + instructionWriter.write((Instruction51l)instruction); + break; + case ArrayPayload: + instructionWriter.write((ArrayPayload)instruction); + break; + case PackedSwitchPayload: + instructionWriter.write((PackedSwitchPayload)instruction); + break; + case SparseSwitchPayload: + instructionWriter.write((SparseSwitchPayload)instruction); + break; + default: + throw new ExceptionWithContext("Unsupported instruction format: %s", + instruction.getOpcode().format); + } + } catch (RuntimeException ex) { + throw new ExceptionWithContext(ex, "Error while writing instruction at code offset 0x%x", codeOffset); } + codeOffset += instruction.getCodeUnits(); } if (tryBlocks.size() > 0) { @@ -1274,4 +1301,16 @@ public abstract class DexWriter< // (https://code.google.com/p/android/issues/detail?id=35304) return (opcodes.api < 17); } + + public abstract class SectionProvider { + @Nonnull public abstract StringSectionType getStringSection(); + @Nonnull public abstract TypeSectionType getTypeSection(); + @Nonnull public abstract ProtoSectionType getProtoSection(); + @Nonnull public abstract FieldSectionType getFieldSection(); + @Nonnull public abstract MethodSectionType getMethodSection(); + @Nonnull public abstract ClassSectionType getClassSection(); + @Nonnull public abstract TypeListSectionType getTypeListSection(); + @Nonnull public abstract AnnotationSectionType getAnnotationSection(); + @Nonnull public abstract AnnotationSetSectionType getAnnotationSetSection(); + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/IndexSection.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/IndexSection.java index 53d14474..8abc776b 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/IndexSection.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/IndexSection.java @@ -38,4 +38,5 @@ import java.util.Map; public interface IndexSection<Key> { int getItemIndex(@Nonnull Key key); @Nonnull Collection<? extends Map.Entry<? extends Key, Integer>> getItems(); + int getItemCount(); } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/MethodSection.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/MethodSection.java index 32e6d6bf..f31e84c8 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/MethodSection.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/MethodSection.java @@ -39,6 +39,7 @@ import javax.annotation.Nonnull; public interface MethodSection<StringKey, TypeKey, ProtoRefKey extends MethodProtoReference, MethodRefKey extends MethodReference, MethodKey> extends IndexSection<MethodRefKey> { + @Nonnull MethodRefKey getMethodReference(@Nonnull MethodKey key); @Nonnull TypeKey getDefiningClass(@Nonnull MethodRefKey key); @Nonnull ProtoRefKey getPrototype(@Nonnull MethodRefKey key); @Nonnull ProtoRefKey getPrototype(@Nonnull MethodKey key); diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BaseBuilderPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BaseBuilderPool.java new file mode 100644 index 00000000..64ba1d7b --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BaseBuilderPool.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2.writer.builder; + +import javax.annotation.Nonnull; + +public class BaseBuilderPool { + @Nonnull protected final DexBuilder dexBuilder; + + public BaseBuilderPool(@Nonnull DexBuilder dexBuilder) { + this.dexBuilder = dexBuilder; + } +} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationPool.java index e16bff0c..37e536cd 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationPool.java @@ -41,14 +41,13 @@ import java.util.Collection; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; -class BuilderAnnotationPool implements AnnotationSection<BuilderStringReference, BuilderTypeReference, - BuilderAnnotation, BuilderAnnotationElement, BuilderEncodedValue> { - @Nonnull private final BuilderContext context; +class BuilderAnnotationPool extends BaseBuilderPool implements AnnotationSection<BuilderStringReference, + BuilderTypeReference, BuilderAnnotation, BuilderAnnotationElement, BuilderEncodedValue> { @Nonnull private final ConcurrentMap<Annotation, BuilderAnnotation> internedItems = Maps.newConcurrentMap(); - BuilderAnnotationPool(@Nonnull BuilderContext context) { - this.context = context; + public BuilderAnnotationPool(@Nonnull DexBuilder dexBuilder) { + super(dexBuilder); } @Nonnull public BuilderAnnotation internAnnotation(@Nonnull Annotation annotation) { @@ -59,8 +58,8 @@ class BuilderAnnotationPool implements AnnotationSection<BuilderStringReference, BuilderAnnotation dexBuilderAnnotation = new BuilderAnnotation( annotation.getVisibility(), - context.typePool.internType(annotation.getType()), - context.internAnnotationElements(annotation.getElements())); + dexBuilder.typeSection.internType(annotation.getType()), + dexBuilder.internAnnotationElements(annotation.getElements())); ret = internedItems.putIfAbsent(dexBuilderAnnotation, dexBuilderAnnotation); return ret==null?dexBuilderAnnotation:ret; } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSet.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSet.java index 43ca7452..ef9a9d63 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSet.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSet.java @@ -39,7 +39,7 @@ import java.util.AbstractSet; import java.util.Iterator; import java.util.Set; -class BuilderAnnotationSet extends AbstractSet<BuilderAnnotation> { +public class BuilderAnnotationSet extends AbstractSet<BuilderAnnotation> { public static final BuilderAnnotationSet EMPTY = new BuilderAnnotationSet(ImmutableSet.<BuilderAnnotation>of()); diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSetPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSetPool.java index 353190fb..45af5cf3 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSetPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSetPool.java @@ -46,13 +46,13 @@ import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.ConcurrentMap; -class BuilderAnnotationSetPool implements AnnotationSetSection<BuilderAnnotation, BuilderAnnotationSet> { - @Nonnull private final BuilderContext context; +class BuilderAnnotationSetPool extends BaseBuilderPool + implements AnnotationSetSection<BuilderAnnotation, BuilderAnnotationSet> { @Nonnull private final ConcurrentMap<Set<? extends Annotation>, BuilderAnnotationSet> internedItems = Maps.newConcurrentMap(); - BuilderAnnotationSetPool(@Nonnull BuilderContext context) { - this.context = context; + public BuilderAnnotationSetPool(@Nonnull DexBuilder dexBuilder) { + super(dexBuilder); } @Nonnull public BuilderAnnotationSet internAnnotationSet(@Nullable Set<? extends Annotation> annotations) { @@ -69,7 +69,7 @@ class BuilderAnnotationSetPool implements AnnotationSetSection<BuilderAnnotation ImmutableSet.copyOf(Iterators.transform(annotations.iterator(), new Function<Annotation, BuilderAnnotation>() { @Nullable @Override public BuilderAnnotation apply(Annotation input) { - return context.annotationPool.internAnnotation(input); + return dexBuilder.annotationSection.internAnnotation(input); } }))); diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassDef.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassDef.java index 10215925..9938a6e0 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassDef.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassDef.java @@ -31,7 +31,6 @@ package org.jf.dexlib2.writer.builder; -import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.collect.*; import org.jf.dexlib2.base.reference.BaseTypeReference; @@ -81,14 +80,10 @@ public class BuilderClassDef extends BaseTypeReference implements ClassDef { this.interfaces = interfaces; this.sourceFile = sourceFile; this.annotations = annotations; - this.staticFields = ImmutableSortedSet.copyOf( - (Iterable<? extends BuilderField>)Iterables.filter(fields, FieldUtil.FIELD_IS_STATIC)); - this.instanceFields = ImmutableSortedSet.copyOf( - (Iterable<? extends BuilderField>)Iterables.filter(fields, FieldUtil.FIELD_IS_INSTANCE)); - this.directMethods = ImmutableSortedSet.copyOf( - (Iterable<? extends BuilderMethod>)Iterables.filter(methods, MethodUtil.METHOD_IS_DIRECT)); - this.virtualMethods = ImmutableSortedSet.copyOf( - (Iterable<? extends BuilderMethod>)Iterables.filter(methods, MethodUtil.METHOD_IS_VIRTUAL)); + this.staticFields = ImmutableSortedSet.copyOf(Iterables.filter(fields, FieldUtil.FIELD_IS_STATIC)); + this.instanceFields = ImmutableSortedSet.copyOf(Iterables.filter(fields, FieldUtil.FIELD_IS_INSTANCE)); + this.directMethods = ImmutableSortedSet.copyOf(Iterables.filter(methods, MethodUtil.METHOD_IS_DIRECT)); + this.virtualMethods = ImmutableSortedSet.copyOf(Iterables.filter(methods, MethodUtil.METHOD_IS_VIRTUAL)); } @Nonnull @Override public String getType() { return type.getType(); } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassPool.java index 29980f32..232b4824 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassPool.java @@ -60,12 +60,14 @@ import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; -public class BuilderClassPool implements ClassSection<BuilderStringReference, BuilderTypeReference, BuilderTypeList, - BuilderClassDef, BuilderField, BuilderMethod, BuilderAnnotationSet, BuilderEncodedValue> { +public class BuilderClassPool extends BaseBuilderPool implements ClassSection<BuilderStringReference, + BuilderTypeReference, BuilderTypeList, BuilderClassDef, BuilderField, BuilderMethod, BuilderAnnotationSet, + BuilderEncodedValue> { @Nonnull private final ConcurrentMap<String, BuilderClassDef> internedItems = Maps.newConcurrentMap(); - BuilderClassPool() { + public BuilderClassPool(@Nonnull DexBuilder dexBuilder) { + super(dexBuilder); } @Nonnull BuilderClassDef internClass(@Nonnull BuilderClassDef classDef) { @@ -441,4 +443,8 @@ public class BuilderClassPool implements ClassSection<BuilderStringReference, Bu } }; } + + @Override public int getItemCount() { + return internedItems.size(); + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderContext.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderContext.java deleted file mode 100644 index e6f8e225..00000000 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderContext.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright 2013, Google Inc. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are - * met: - * - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above - * copyright notice, this list of conditions and the following disclaimer - * in the documentation and/or other materials provided with the - * distribution. - * * Neither the name of Google Inc. nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.jf.dexlib2.writer.builder; - -import com.google.common.base.Function; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterators; -import org.jf.dexlib2.ValueType; -import org.jf.dexlib2.iface.AnnotationElement; -import org.jf.dexlib2.iface.value.*; -import org.jf.dexlib2.writer.builder.BuilderEncodedValues.*; -import org.jf.util.ExceptionWithContext; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Set; - -class BuilderContext { - // keep our own local references to the various pools, using the Builder specific pool type; - @Nonnull final BuilderStringPool stringPool; - @Nonnull final BuilderTypePool typePool; - @Nonnull final BuilderFieldPool fieldPool; - @Nonnull final BuilderMethodPool methodPool; - @Nonnull final BuilderProtoPool protoPool; - @Nonnull final BuilderClassPool classPool; - - @Nonnull final BuilderTypeListPool typeListPool; - @Nonnull final BuilderAnnotationPool annotationPool; - @Nonnull final BuilderAnnotationSetPool annotationSetPool; - - - BuilderContext() { - this.stringPool = new BuilderStringPool(); - this.typePool = new BuilderTypePool(this); - this.fieldPool = new BuilderFieldPool(this); - this.methodPool = new BuilderMethodPool(this); - this.protoPool = new BuilderProtoPool(this); - this.classPool = new BuilderClassPool(); - - this.typeListPool = new BuilderTypeListPool(this); - this.annotationPool = new BuilderAnnotationPool(this); - this.annotationSetPool = new BuilderAnnotationSetPool(this); - } - - @Nonnull Set<? extends BuilderAnnotationElement> internAnnotationElements( - @Nonnull Set<? extends AnnotationElement> elements) { - return ImmutableSet.copyOf( - Iterators.transform(elements.iterator(), - new Function<AnnotationElement, BuilderAnnotationElement>() { - @Nullable @Override - public BuilderAnnotationElement apply(AnnotationElement input) { - return internAnnotationElement(input); - } - })); - } - - @Nonnull private BuilderAnnotationElement internAnnotationElement(@Nonnull AnnotationElement annotationElement) { - return new BuilderAnnotationElement(stringPool.internString(annotationElement.getName()), - internEncodedValue(annotationElement.getValue())); - } - - @Nullable BuilderEncodedValue internNullableEncodedValue(@Nullable EncodedValue encodedValue) { - if (encodedValue == null) { - return null; - } - return internEncodedValue(encodedValue); - } - - @Nonnull private BuilderEncodedValue internEncodedValue(@Nonnull EncodedValue encodedValue) { - switch (encodedValue.getValueType()) { - case ValueType.ANNOTATION: - return internAnnotationEncodedValue((AnnotationEncodedValue)encodedValue); - case ValueType.ARRAY: - return internArrayEncodedValue((ArrayEncodedValue)encodedValue); - case ValueType.BOOLEAN: - boolean value = ((BooleanEncodedValue)encodedValue).getValue(); - return value?BuilderBooleanEncodedValue.TRUE_VALUE:BuilderBooleanEncodedValue.FALSE_VALUE; - case ValueType.BYTE: - return new BuilderByteEncodedValue(((ByteEncodedValue)encodedValue).getValue()); - case ValueType.CHAR: - return new BuilderCharEncodedValue(((CharEncodedValue)encodedValue).getValue()); - case ValueType.DOUBLE: - return new BuilderDoubleEncodedValue(((DoubleEncodedValue)encodedValue).getValue()); - case ValueType.ENUM: - return internEnumEncodedValue((EnumEncodedValue)encodedValue); - case ValueType.FIELD: - return internFieldEncodedValue((FieldEncodedValue)encodedValue); - case ValueType.FLOAT: - return new BuilderFloatEncodedValue(((FloatEncodedValue)encodedValue).getValue()); - case ValueType.INT: - return new BuilderIntEncodedValue(((IntEncodedValue)encodedValue).getValue()); - case ValueType.LONG: - return new BuilderLongEncodedValue(((LongEncodedValue)encodedValue).getValue()); - case ValueType.METHOD: - return internMethodEncodedValue((MethodEncodedValue)encodedValue); - case ValueType.NULL: - return BuilderNullEncodedValue.INSTANCE; - case ValueType.SHORT: - return new BuilderShortEncodedValue(((ShortEncodedValue)encodedValue).getValue()); - case ValueType.STRING: - return internStringEncodedValue((StringEncodedValue)encodedValue); - case ValueType.TYPE: - return internTypeEncodedValue((TypeEncodedValue)encodedValue); - default: - throw new ExceptionWithContext("Unexpected encoded value type: %d", encodedValue.getValueType()); - } - } - - @Nonnull private BuilderAnnotationEncodedValue internAnnotationEncodedValue(@Nonnull AnnotationEncodedValue value) { - return new BuilderAnnotationEncodedValue( - typePool.internType(value.getType()), - internAnnotationElements(value.getElements())); - } - - @Nonnull private BuilderArrayEncodedValue internArrayEncodedValue(@Nonnull ArrayEncodedValue value) { - return new BuilderArrayEncodedValue( - ImmutableList.copyOf( - Iterators.transform(value.getValue().iterator(), - new Function<EncodedValue, BuilderEncodedValue>() { - @Nullable @Override public BuilderEncodedValue apply(EncodedValue input) { - return internEncodedValue(input); - } - }))); - } - - @Nonnull private BuilderEnumEncodedValue internEnumEncodedValue(@Nonnull EnumEncodedValue value) { - return new BuilderEnumEncodedValue(fieldPool.internField(value.getValue())); - } - - @Nonnull private BuilderFieldEncodedValue internFieldEncodedValue(@Nonnull FieldEncodedValue value) { - return new BuilderFieldEncodedValue(fieldPool.internField(value.getValue())); - } - - @Nonnull private BuilderMethodEncodedValue internMethodEncodedValue(@Nonnull MethodEncodedValue value) { - return new BuilderMethodEncodedValue(methodPool.internMethod(value.getValue())); - } - - @Nonnull private BuilderStringEncodedValue internStringEncodedValue(@Nonnull StringEncodedValue string) { - return new BuilderStringEncodedValue(stringPool.internString(string.getValue())); - } - - @Nonnull private BuilderTypeEncodedValue internTypeEncodedValue(@Nonnull TypeEncodedValue type) { - return new BuilderTypeEncodedValue(typePool.internType(type.getValue())); - } -} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderFieldPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderFieldPool.java index 7a90649f..3bc65cd8 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderFieldPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderFieldPool.java @@ -41,14 +41,13 @@ import java.util.Collection; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; -public class BuilderFieldPool +public class BuilderFieldPool extends BaseBuilderPool implements FieldSection<BuilderStringReference, BuilderTypeReference, BuilderFieldReference, BuilderField> { - @Nonnull private final BuilderContext context; @Nonnull private final ConcurrentMap<FieldReference, BuilderFieldReference> internedItems = Maps.newConcurrentMap(); - BuilderFieldPool(@Nonnull BuilderContext context) { - this.context = context; + public BuilderFieldPool(@Nonnull DexBuilder dexBuilder) { + super(dexBuilder); } @Nonnull BuilderFieldReference internField(@Nonnull String definingClass, String name, String type) { @@ -63,9 +62,9 @@ public class BuilderFieldPool } BuilderFieldReference dexPoolFieldReference = new BuilderFieldReference( - context.typePool.internType(fieldReference.getDefiningClass()), - context.stringPool.internString(fieldReference.getName()), - context.typePool.internType(fieldReference.getType())); + dexBuilder.typeSection.internType(fieldReference.getDefiningClass()), + dexBuilder.stringSection.internString(fieldReference.getName()), + dexBuilder.typeSection.internType(fieldReference.getType())); ret = internedItems.putIfAbsent(dexPoolFieldReference, dexPoolFieldReference); return ret==null?dexPoolFieldReference:ret; } @@ -104,4 +103,8 @@ public class BuilderFieldPool } }; } + + @Override public int getItemCount() { + return internedItems.size(); + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderMethodPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderMethodPool.java index 2c5dd816..7f937fdc 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderMethodPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderMethodPool.java @@ -42,14 +42,13 @@ import java.util.List; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; -class BuilderMethodPool implements MethodSection<BuilderStringReference, BuilderTypeReference, +class BuilderMethodPool extends BaseBuilderPool implements MethodSection<BuilderStringReference, BuilderTypeReference, BuilderMethodProtoReference, BuilderMethodReference, BuilderMethod>{ - @Nonnull private final BuilderContext context; @Nonnull private final ConcurrentMap<MethodReference, BuilderMethodReference> internedItems = Maps.newConcurrentMap(); - BuilderMethodPool(@Nonnull BuilderContext context) { - this.context = context; + public BuilderMethodPool(@Nonnull DexBuilder dexBuilder) { + super(dexBuilder); } @Nonnull public BuilderMethodReference internMethod(@Nonnull MethodReference methodReference) { @@ -59,9 +58,9 @@ class BuilderMethodPool implements MethodSection<BuilderStringReference, Builder } BuilderMethodReference dexPoolMethodReference = new BuilderMethodReference( - context.typePool.internType(methodReference.getDefiningClass()), - context.stringPool.internString(methodReference.getName()), - context.protoPool.internMethodProto(methodReference)); + dexBuilder.typeSection.internType(methodReference.getDefiningClass()), + dexBuilder.stringSection.internString(methodReference.getName()), + dexBuilder.protoSection.internMethodProto(methodReference)); ret = internedItems.putIfAbsent(dexPoolMethodReference, dexPoolMethodReference); return ret==null?dexPoolMethodReference:ret; } @@ -72,6 +71,10 @@ class BuilderMethodPool implements MethodSection<BuilderStringReference, Builder return internMethod(new MethodKey(definingClass, name, parameters, returnType)); } + @Nonnull @Override public BuilderMethodReference getMethodReference(@Nonnull BuilderMethod builderMethod) { + return builderMethod.methodReference; + } + @Nonnull @Override public BuilderTypeReference getDefiningClass(@Nonnull BuilderMethodReference key) { return key.definingClass; @@ -112,6 +115,10 @@ class BuilderMethodPool implements MethodSection<BuilderStringReference, Builder }; } + @Override public int getItemCount() { + return internedItems.size(); + } + private static class MethodKey extends BaseMethodReference implements MethodReference { @Nonnull private final String definingClass; @Nonnull private final String name; diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderProtoPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderProtoPool.java index de19fa30..969f2433 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderProtoPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderProtoPool.java @@ -44,14 +44,13 @@ import java.util.Collection; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; -class BuilderProtoPool +class BuilderProtoPool extends BaseBuilderPool implements ProtoSection<BuilderStringReference, BuilderTypeReference, BuilderMethodProtoReference, BuilderTypeList> { - @Nonnull private final BuilderContext context; @Nonnull private final ConcurrentMap<MethodProtoReference, BuilderMethodProtoReference> internedItems = Maps.newConcurrentMap(); - BuilderProtoPool(@Nonnull BuilderContext context) { - this.context = context; + public BuilderProtoPool(@Nonnull DexBuilder dexBuilder) { + super(dexBuilder); } @Nonnull public BuilderMethodProtoReference internMethodProto(@Nonnull MethodProtoReference methodProto) { @@ -61,10 +60,10 @@ class BuilderProtoPool } BuilderMethodProtoReference protoReference = new BuilderMethodProtoReference( - context.stringPool.internString(MethodUtil.getShorty( + dexBuilder.stringSection.internString(MethodUtil.getShorty( methodProto.getParameterTypes(), methodProto.getReturnType())), - context.typeListPool.internTypeList(methodProto.getParameterTypes()), - context.typePool.internType(methodProto.getReturnType())); + dexBuilder.typeListSection.internTypeList(methodProto.getParameterTypes()), + dexBuilder.typeSection.internType(methodProto.getReturnType())); ret = internedItems.putIfAbsent(protoReference, protoReference); return ret==null?protoReference:ret; } @@ -103,4 +102,8 @@ class BuilderProtoPool } }; } + + @Override public int getItemCount() { + return internedItems.size(); + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderStringPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderStringPool.java index 6b60e9f8..95fe86fb 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderStringPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderStringPool.java @@ -86,4 +86,8 @@ class BuilderStringPool implements StringSection<BuilderStringReference, Builder } }; } + + @Override public int getItemCount() { + return internedItems.size(); + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypeListPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypeListPool.java index 7b189fdb..604e39ce 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypeListPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypeListPool.java @@ -45,13 +45,12 @@ import java.util.List; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; -class BuilderTypeListPool implements TypeListSection<BuilderTypeReference, BuilderTypeList> { - @Nonnull private final BuilderContext context; +class BuilderTypeListPool extends BaseBuilderPool implements TypeListSection<BuilderTypeReference, BuilderTypeList> { @Nonnull private final ConcurrentMap<List<? extends CharSequence>, BuilderTypeList> internedItems = Maps.newConcurrentMap(); - BuilderTypeListPool(@Nonnull BuilderContext context) { - this.context = context; + public BuilderTypeListPool(@Nonnull DexBuilder dexBuilder) { + super(dexBuilder); } @Nonnull public BuilderTypeList internTypeList(@Nullable List<? extends CharSequence> types) { @@ -67,7 +66,7 @@ class BuilderTypeListPool implements TypeListSection<BuilderTypeReference, Build BuilderTypeList typeList = new BuilderTypeList( ImmutableList.copyOf(Iterables.transform(types, new Function<CharSequence, BuilderTypeReference>() { @Nonnull @Override public BuilderTypeReference apply(CharSequence input) { - return context.typePool.internType(input.toString()); + return dexBuilder.typeSection.internType(input.toString()); } }))); diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypePool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypePool.java index 29871fc9..b9476de0 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypePool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypePool.java @@ -41,12 +41,12 @@ import java.util.Collection; import java.util.Map.Entry; import java.util.concurrent.ConcurrentMap; -class BuilderTypePool implements TypeSection<BuilderStringReference, BuilderTypeReference, BuilderTypeReference> { - @Nonnull private final BuilderContext context; +class BuilderTypePool extends BaseBuilderPool + implements TypeSection<BuilderStringReference, BuilderTypeReference, BuilderTypeReference> { @Nonnull private final ConcurrentMap<String, BuilderTypeReference> internedItems = Maps.newConcurrentMap(); - BuilderTypePool(@Nonnull BuilderContext context) { - this.context = context; + public BuilderTypePool(@Nonnull DexBuilder dexBuilder) { + super(dexBuilder); } @Nonnull public BuilderTypeReference internType(@Nonnull String type) { @@ -54,7 +54,7 @@ class BuilderTypePool implements TypeSection<BuilderStringReference, BuilderType if (ret != null) { return ret; } - BuilderStringReference stringRef = context.stringPool.internString(type); + BuilderStringReference stringRef = dexBuilder.stringSection.internString(type); BuilderTypeReference typeReference = new BuilderTypeReference(stringRef); ret = internedItems.putIfAbsent(type, typeReference); return ret==null?typeReference:ret; @@ -92,4 +92,8 @@ class BuilderTypePool implements TypeSection<BuilderStringReference, BuilderType } }; } + + @Override public int getItemCount() { + return internedItems.size(); + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/DexBuilder.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/DexBuilder.java index b7507fa6..25938fe0 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/DexBuilder.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/DexBuilder.java @@ -33,11 +33,13 @@ package org.jf.dexlib2.writer.builder; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterators; import com.google.common.collect.Sets; import org.jf.dexlib2.Opcodes; import org.jf.dexlib2.ValueType; import org.jf.dexlib2.iface.Annotation; +import org.jf.dexlib2.iface.AnnotationElement; import org.jf.dexlib2.iface.MethodImplementation; import org.jf.dexlib2.iface.MethodParameter; import org.jf.dexlib2.iface.reference.*; @@ -56,32 +58,16 @@ import java.util.Set; public class DexBuilder extends DexWriter<BuilderStringReference, BuilderStringReference, BuilderTypeReference, BuilderTypeReference, BuilderMethodProtoReference, BuilderFieldReference, BuilderMethodReference, BuilderClassDef, BuilderAnnotation, BuilderAnnotationSet, BuilderTypeList, BuilderField, BuilderMethod, - BuilderEncodedValue, BuilderAnnotationElement> { + BuilderEncodedValue, BuilderAnnotationElement, BuilderStringPool, BuilderTypePool, BuilderProtoPool, + BuilderFieldPool, BuilderMethodPool, BuilderClassPool, BuilderTypeListPool, BuilderAnnotationPool, + BuilderAnnotationSetPool> { - @Nonnull private final BuilderContext context; - - @Nonnull public static DexBuilder makeDexBuilder() { - BuilderContext context = new BuilderContext(); - return new DexBuilder(Opcodes.forApi(20), context); - } - - @Deprecated - @Nonnull - public static DexBuilder makeDexBuilder(int api) { - BuilderContext context = new BuilderContext(); - return new DexBuilder(Opcodes.forApi(api), context); - } - - @Nonnull public static DexBuilder makeDexBuilder(@Nonnull Opcodes opcodes) { - BuilderContext context = new BuilderContext(); - return new DexBuilder(opcodes, context); + public DexBuilder(@Nonnull Opcodes opcodes) { + super(opcodes); } - private DexBuilder(@Nonnull Opcodes opcodes, @Nonnull BuilderContext context) { - super(opcodes, context.stringPool, context.typePool, context.protoPool, - context.fieldPool, context.methodPool, context.classPool, context.typeListPool, context.annotationPool, - context.annotationSetPool); - this.context = context; + @Nonnull @Override protected SectionProvider getSectionProvider() { + return new DexBuilderSectionProvider(); } @Nonnull public BuilderField internField(@Nonnull String definingClass, @@ -90,10 +76,10 @@ public class DexBuilder extends DexWriter<BuilderStringReference, BuilderStringR int accessFlags, @Nullable EncodedValue initialValue, @Nonnull Set<? extends Annotation> annotations) { - return new BuilderField(context.fieldPool.internField(definingClass, name, type), + return new BuilderField(fieldSection.internField(definingClass, name, type), accessFlags, - context.internNullableEncodedValue(initialValue), - context.annotationSetPool.internAnnotationSet(annotations)); + internNullableEncodedValue(initialValue), + annotationSetSection.internAnnotationSet(annotations)); } @Nonnull public BuilderMethod internMethod(@Nonnull String definingClass, @@ -106,10 +92,10 @@ public class DexBuilder extends DexWriter<BuilderStringReference, BuilderStringR if (parameters == null) { parameters = ImmutableList.of(); } - return new BuilderMethod(context.methodPool.internMethod(definingClass, name, parameters, returnType), + return new BuilderMethod(methodSection.internMethod(definingClass, name, parameters, returnType), internMethodParameters(parameters), accessFlags, - context.annotationSetPool.internAnnotationSet(annotations), + annotationSetSection.internAnnotationSet(annotations), methodImplementation); } @@ -136,18 +122,18 @@ public class DexBuilder extends DexWriter<BuilderStringReference, BuilderStringR } } - return context.classPool.internClass(new BuilderClassDef(context.typePool.internType(type), + return classSection.internClass(new BuilderClassDef(typeSection.internType(type), accessFlags, - context.typePool.internNullableType(superclass), - context.typeListPool.internTypeList(interfaces), - context.stringPool.internNullableString(sourceFile), - context.annotationSetPool.internAnnotationSet(annotations), + typeSection.internNullableType(superclass), + typeListSection.internTypeList(interfaces), + stringSection.internNullableString(sourceFile), + annotationSetSection.internAnnotationSet(annotations), fields, methods)); } @Nonnull public BuilderStringReference internStringReference(@Nonnull String string) { - return context.stringPool.internString(string); + return stringSection.internString(string); } @Nullable public BuilderStringReference internNullableStringReference(@Nullable String string) { @@ -158,7 +144,7 @@ public class DexBuilder extends DexWriter<BuilderStringReference, BuilderStringR } @Nonnull public BuilderTypeReference internTypeReference(@Nonnull String type) { - return context.typePool.internType(type); + return typeSection.internType(type); } @Nullable public BuilderTypeReference internNullableTypeReference(@Nullable String type) { @@ -169,15 +155,15 @@ public class DexBuilder extends DexWriter<BuilderStringReference, BuilderStringR } @Nonnull public BuilderFieldReference internFieldReference(@Nonnull FieldReference field) { - return context.fieldPool.internField(field); + return fieldSection.internField(field); } @Nonnull public BuilderMethodReference internMethodReference(@Nonnull MethodReference method) { - return context.methodPool.internMethod(method); + return methodSection.internMethod(method); } @Nonnull public BuilderMethodProtoReference internMethodProtoReference(@Nonnull MethodProtoReference methodProto) { - return context.protoPool.internMethodProto(methodProto); + return protoSection.internMethodProto(methodProto); } @Nonnull public BuilderReference internReference(@Nonnull Reference reference) { @@ -214,9 +200,9 @@ public class DexBuilder extends DexWriter<BuilderStringReference, BuilderStringR @Nonnull private BuilderMethodParameter internMethodParameter(@Nonnull MethodParameter methodParameter) { return new BuilderMethodParameter( - context.typePool.internType(methodParameter.getType()), - context.stringPool.internNullableString(methodParameter.getName()), - context.annotationSetPool.internAnnotationSet(methodParameter.getAnnotations())); + typeSection.internType(methodParameter.getType()), + stringSection.internNullableString(methodParameter.getName()), + annotationSetSection.internAnnotationSet(methodParameter.getAnnotations())); } @Override protected void writeEncodedValue(@Nonnull InternalEncodedValueWriter writer, @@ -276,4 +262,143 @@ public class DexBuilder extends DexWriter<BuilderStringReference, BuilderStringR throw new ExceptionWithContext("Unrecognized value type: %d", encodedValue.getValueType()); } } + + @Nonnull Set<? extends BuilderAnnotationElement> internAnnotationElements( + @Nonnull Set<? extends AnnotationElement> elements) { + return ImmutableSet.copyOf( + Iterators.transform(elements.iterator(), + new Function<AnnotationElement, BuilderAnnotationElement>() { + @Nullable @Override + public BuilderAnnotationElement apply(AnnotationElement input) { + return internAnnotationElement(input); + } + })); + } + + @Nonnull private BuilderAnnotationElement internAnnotationElement(@Nonnull AnnotationElement annotationElement) { + return new BuilderAnnotationElement(stringSection.internString(annotationElement.getName()), + internEncodedValue(annotationElement.getValue())); + } + + @Nullable BuilderEncodedValue internNullableEncodedValue(@Nullable EncodedValue encodedValue) { + if (encodedValue == null) { + return null; + } + return internEncodedValue(encodedValue); + } + + @Nonnull private BuilderEncodedValue internEncodedValue(@Nonnull EncodedValue encodedValue) { + switch (encodedValue.getValueType()) { + case ValueType.ANNOTATION: + return internAnnotationEncodedValue((AnnotationEncodedValue)encodedValue); + case ValueType.ARRAY: + return internArrayEncodedValue((ArrayEncodedValue)encodedValue); + case ValueType.BOOLEAN: + boolean value = ((BooleanEncodedValue)encodedValue).getValue(); + return value?BuilderBooleanEncodedValue.TRUE_VALUE:BuilderBooleanEncodedValue.FALSE_VALUE; + case ValueType.BYTE: + return new BuilderByteEncodedValue(((ByteEncodedValue)encodedValue).getValue()); + case ValueType.CHAR: + return new BuilderCharEncodedValue(((CharEncodedValue)encodedValue).getValue()); + case ValueType.DOUBLE: + return new BuilderDoubleEncodedValue(((DoubleEncodedValue)encodedValue).getValue()); + case ValueType.ENUM: + return internEnumEncodedValue((EnumEncodedValue)encodedValue); + case ValueType.FIELD: + return internFieldEncodedValue((FieldEncodedValue)encodedValue); + case ValueType.FLOAT: + return new BuilderFloatEncodedValue(((FloatEncodedValue)encodedValue).getValue()); + case ValueType.INT: + return new BuilderIntEncodedValue(((IntEncodedValue)encodedValue).getValue()); + case ValueType.LONG: + return new BuilderLongEncodedValue(((LongEncodedValue)encodedValue).getValue()); + case ValueType.METHOD: + return internMethodEncodedValue((MethodEncodedValue)encodedValue); + case ValueType.NULL: + return BuilderNullEncodedValue.INSTANCE; + case ValueType.SHORT: + return new BuilderShortEncodedValue(((ShortEncodedValue)encodedValue).getValue()); + case ValueType.STRING: + return internStringEncodedValue((StringEncodedValue)encodedValue); + case ValueType.TYPE: + return internTypeEncodedValue((TypeEncodedValue)encodedValue); + default: + throw new ExceptionWithContext("Unexpected encoded value type: %d", encodedValue.getValueType()); + } + } + + @Nonnull private BuilderAnnotationEncodedValue internAnnotationEncodedValue(@Nonnull AnnotationEncodedValue value) { + return new BuilderAnnotationEncodedValue( + typeSection.internType(value.getType()), + internAnnotationElements(value.getElements())); + } + + @Nonnull private BuilderArrayEncodedValue internArrayEncodedValue(@Nonnull ArrayEncodedValue value) { + return new BuilderArrayEncodedValue( + ImmutableList.copyOf( + Iterators.transform(value.getValue().iterator(), + new Function<EncodedValue, BuilderEncodedValue>() { + @Nullable @Override public BuilderEncodedValue apply(EncodedValue input) { + return internEncodedValue(input); + } + }))); + } + + @Nonnull private BuilderEnumEncodedValue internEnumEncodedValue(@Nonnull EnumEncodedValue value) { + return new BuilderEnumEncodedValue(fieldSection.internField(value.getValue())); + } + + @Nonnull private BuilderFieldEncodedValue internFieldEncodedValue(@Nonnull FieldEncodedValue value) { + return new BuilderFieldEncodedValue(fieldSection.internField(value.getValue())); + } + + @Nonnull private BuilderMethodEncodedValue internMethodEncodedValue(@Nonnull MethodEncodedValue value) { + return new BuilderMethodEncodedValue(methodSection.internMethod(value.getValue())); + } + + @Nonnull private BuilderStringEncodedValue internStringEncodedValue(@Nonnull StringEncodedValue string) { + return new BuilderStringEncodedValue(stringSection.internString(string.getValue())); + } + + @Nonnull private BuilderTypeEncodedValue internTypeEncodedValue(@Nonnull TypeEncodedValue type) { + return new BuilderTypeEncodedValue(typeSection.internType(type.getValue())); + } + + protected class DexBuilderSectionProvider extends SectionProvider { + @Nonnull @Override public BuilderStringPool getStringSection() { + return new BuilderStringPool(); + } + + @Nonnull @Override public BuilderTypePool getTypeSection() { + return new BuilderTypePool(DexBuilder.this); + } + + @Nonnull @Override public BuilderProtoPool getProtoSection() { + return new BuilderProtoPool(DexBuilder.this); + } + + @Nonnull @Override public BuilderFieldPool getFieldSection() { + return new BuilderFieldPool(DexBuilder.this); + } + + @Nonnull @Override public BuilderMethodPool getMethodSection() { + return new BuilderMethodPool(DexBuilder.this); + } + + @Nonnull @Override public BuilderClassPool getClassSection() { + return new BuilderClassPool(DexBuilder.this); + } + + @Nonnull @Override public BuilderTypeListPool getTypeListSection() { + return new BuilderTypeListPool(DexBuilder.this); + } + + @Nonnull @Override public BuilderAnnotationPool getAnnotationSection() { + return new BuilderAnnotationPool(DexBuilder.this); + } + + @Nonnull @Override public BuilderAnnotationSetPool getAnnotationSetSection() { + return new BuilderAnnotationSetPool(DexBuilder.this); + } + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationPool.java index c4cfa390..b2db8890 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationPool.java @@ -41,26 +41,18 @@ import java.util.Collection; public class AnnotationPool extends BaseOffsetPool<Annotation> implements AnnotationSection<CharSequence, CharSequence, Annotation, AnnotationElement, EncodedValue> { - @Nonnull StringPool stringPool; - @Nonnull TypePool typePool; - @Nonnull FieldPool fieldPool; - @Nonnull MethodPool methodPool; - public AnnotationPool(@Nonnull StringPool stringPool, @Nonnull TypePool typePool, - @Nonnull FieldPool fieldPool, @Nonnull MethodPool methodPool) { - this.stringPool = stringPool; - this.typePool = typePool; - this.fieldPool = fieldPool; - this.methodPool = methodPool; + public AnnotationPool(@Nonnull DexPool dexPool) { + super(dexPool); } public void intern(@Nonnull Annotation annotation) { Integer prev = internedItems.put(annotation, 0); if (prev == null) { - typePool.intern(annotation.getType()); + dexPool.typeSection.intern(annotation.getType()); for (AnnotationElement element: annotation.getElements()) { - stringPool.intern(element.getName()); - DexPool.internEncodedValue(element.getValue(), stringPool, typePool, fieldPool, methodPool); + dexPool.stringSection.intern(element.getName()); + dexPool.internEncodedValue(element.getValue()); } } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationSetPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationSetPool.java index 6512dcb4..2170b95b 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationSetPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationSetPool.java @@ -40,10 +40,9 @@ import java.util.Set; public class AnnotationSetPool extends BaseNullableOffsetPool<Set<? extends Annotation>> implements AnnotationSetSection<Annotation, Set<? extends Annotation>> { - @Nonnull private final AnnotationPool annotationPool; - public AnnotationSetPool(@Nonnull AnnotationPool annotationPool) { - this.annotationPool = annotationPool; + public AnnotationSetPool(@Nonnull DexPool dexPool) { + super(dexPool); } public void intern(@Nonnull Set<? extends Annotation> annotationSet) { @@ -51,7 +50,7 @@ public class AnnotationSetPool extends BaseNullableOffsetPool<Set<? extends Anno Integer prev = internedItems.put(annotationSet, 0); if (prev == null) { for (Annotation annotation: annotationSet) { - annotationPool.intern(annotation); + dexPool.annotationSection.intern(annotation); } } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseIndexPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseIndexPool.java index 01109ad4..c07dcf1a 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseIndexPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseIndexPool.java @@ -31,7 +31,6 @@ package org.jf.dexlib2.writer.pool; -import com.google.common.collect.Maps; import org.jf.dexlib2.writer.IndexSection; import org.jf.util.ExceptionWithContext; @@ -39,8 +38,11 @@ import javax.annotation.Nonnull; import java.util.Collection; import java.util.Map; -public abstract class BaseIndexPool<Key> implements IndexSection<Key> { - @Nonnull protected final Map<Key, Integer> internedItems = Maps.newHashMap(); +public abstract class BaseIndexPool<Key> extends BasePool<Key, Integer> implements IndexSection<Key> { + + public BaseIndexPool(@Nonnull DexPool dexPool) { + super(dexPool); + } @Nonnull @Override public Collection<? extends Map.Entry<? extends Key, Integer>> getItems() { return internedItems.entrySet(); diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseNullableOffsetPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseNullableOffsetPool.java index ed9dbb6a..b04060a3 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseNullableOffsetPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseNullableOffsetPool.java @@ -34,10 +34,16 @@ package org.jf.dexlib2.writer.pool; import org.jf.dexlib2.writer.DexWriter; import org.jf.dexlib2.writer.NullableOffsetSection; +import javax.annotation.Nonnull; import javax.annotation.Nullable; public abstract class BaseNullableOffsetPool<Key> extends BaseOffsetPool<Key> implements NullableOffsetSection<Key> { + + public BaseNullableOffsetPool(@Nonnull DexPool dexPool) { + super(dexPool); + } + @Override public int getNullableItemOffset(@Nullable Key key) { if (key == null) { return DexWriter.NO_OFFSET; diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseOffsetPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseOffsetPool.java index e66a50ad..789c9547 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseOffsetPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseOffsetPool.java @@ -31,7 +31,6 @@ package org.jf.dexlib2.writer.pool; -import com.google.common.collect.Maps; import org.jf.dexlib2.writer.OffsetSection; import org.jf.util.ExceptionWithContext; @@ -39,8 +38,11 @@ import javax.annotation.Nonnull; import java.util.Collection; import java.util.Map; -public abstract class BaseOffsetPool<Key> implements OffsetSection<Key> { - @Nonnull protected final Map<Key, Integer> internedItems = Maps.newHashMap(); +public abstract class BaseOffsetPool<Key> extends BasePool<Key, Integer> implements OffsetSection<Key> { + + public BaseOffsetPool(@Nonnull DexPool dexPool) { + super(dexPool); + } @Nonnull @Override public Collection<? extends Map.Entry<? extends Key, Integer>> getItems() { return internedItems.entrySet(); diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BasePool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BasePool.java new file mode 100644 index 00000000..4fa18102 --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BasePool.java @@ -0,0 +1,75 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2.writer.pool; + +import com.google.common.collect.Maps; + +import javax.annotation.Nonnull; +import java.util.Iterator; +import java.util.Map; + +public class BasePool<Key, Value> implements Markable { + @Nonnull protected final DexPool dexPool; + @Nonnull protected final Map<Key, Value> internedItems = Maps.newLinkedHashMap(); + private int markedItemCount = -1; + + public BasePool(@Nonnull DexPool dexPool) { + this.dexPool = dexPool; + } + + public void mark() { + markedItemCount = internedItems.size(); + } + + public void reset() { + if (markedItemCount < 0) { + throw new IllegalStateException("mark() must be called before calling reset()"); + } + + if (markedItemCount == internedItems.size()) { + return; + } + + Iterator<Key> keys = internedItems.keySet().iterator(); + for (int i=0; i<markedItemCount; i++) { + keys.next(); + } + while (keys.hasNext()) { + keys.next(); + keys.remove(); + } + } + + public int getItemCount() { + return internedItems.size(); + } +} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ClassPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ClassPool.java index 0389973a..7c1d6811 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ClassPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ClassPool.java @@ -58,30 +58,12 @@ import java.io.IOException; import java.util.*; import java.util.Map.Entry; -public class ClassPool implements ClassSection<CharSequence, CharSequence, +public class ClassPool extends BasePool<String, PoolClassDef> implements ClassSection<CharSequence, CharSequence, TypeListPool.Key<? extends Collection<? extends CharSequence>>, PoolClassDef, Field, PoolMethod, Set<? extends Annotation>, EncodedValue> { - @Nonnull private HashMap<String, PoolClassDef> internedItems = Maps.newHashMap(); - - @Nonnull private final StringPool stringPool; - @Nonnull private final TypePool typePool; - @Nonnull private final FieldPool fieldPool; - @Nonnull private final MethodPool methodPool; - @Nonnull private final AnnotationSetPool annotationSetPool; - @Nonnull private final TypeListPool typeListPool; - - public ClassPool(@Nonnull StringPool stringPool, - @Nonnull TypePool typePool, - @Nonnull FieldPool fieldPool, - @Nonnull MethodPool methodPool, - @Nonnull AnnotationSetPool annotationSetPool, - @Nonnull TypeListPool typeListPool) { - this.stringPool = stringPool; - this.typePool = typePool; - this.fieldPool = fieldPool; - this.methodPool = methodPool; - this.annotationSetPool = annotationSetPool; - this.typeListPool = typeListPool; + + public ClassPool(@Nonnull DexPool dexPool) { + super(dexPool); } public void intern(@Nonnull ClassDef classDef) { @@ -92,10 +74,10 @@ public class ClassPool implements ClassSection<CharSequence, CharSequence, throw new ExceptionWithContext("Class %s has already been interned", poolClassDef.getType()); } - typePool.intern(poolClassDef.getType()); - typePool.internNullable(poolClassDef.getSuperclass()); - typeListPool.intern(poolClassDef.getInterfaces()); - stringPool.internNullable(poolClassDef.getSourceFile()); + dexPool.typeSection.intern(poolClassDef.getType()); + dexPool.typeSection.internNullable(poolClassDef.getSuperclass()); + dexPool.typeListSection.intern(poolClassDef.getInterfaces()); + dexPool.stringSection.internNullable(poolClassDef.getSourceFile()); HashSet<String> fields = new HashSet<String>(); for (Field field: poolClassDef.getFields()) { @@ -104,14 +86,14 @@ public class ClassPool implements ClassSection<CharSequence, CharSequence, throw new ExceptionWithContext("Multiple definitions for field %s->%s", poolClassDef.getType(), fieldDescriptor); } - fieldPool.intern(field); + dexPool.fieldSection.intern(field); EncodedValue initialValue = field.getInitialValue(); if (initialValue != null) { - DexPool.internEncodedValue(initialValue, stringPool, typePool, fieldPool, methodPool); + dexPool.internEncodedValue(initialValue); } - annotationSetPool.intern(field.getAnnotations()); + dexPool.annotationSetSection.intern(field.getAnnotations()); } HashSet<String> methods = new HashSet<String>(); @@ -121,17 +103,17 @@ public class ClassPool implements ClassSection<CharSequence, CharSequence, throw new ExceptionWithContext("Multiple definitions for method %s->%s", poolClassDef.getType(), methodDescriptor); } - methodPool.intern(method); + dexPool.methodSection.intern(method); internCode(method); internDebug(method); - annotationSetPool.intern(method.getAnnotations()); + dexPool.annotationSetSection.intern(method.getAnnotations()); for (MethodParameter parameter: method.getParameters()) { - annotationSetPool.intern(parameter.getAnnotations()); + dexPool.annotationSetSection.intern(parameter.getAnnotations()); } } - annotationSetPool.intern(poolClassDef.getAnnotations()); + dexPool.annotationSetSection.intern(poolClassDef.getAnnotations()); } private void internCode(@Nonnull Method method) { @@ -146,16 +128,16 @@ public class ClassPool implements ClassSection<CharSequence, CharSequence, Reference reference = ((ReferenceInstruction)instruction).getReference(); switch (instruction.getOpcode().referenceType) { case ReferenceType.STRING: - stringPool.intern((StringReference)reference); + dexPool.stringSection.intern((StringReference)reference); break; case ReferenceType.TYPE: - typePool.intern((TypeReference)reference); + dexPool.typeSection.intern((TypeReference)reference); break; case ReferenceType.FIELD: - fieldPool.intern((FieldReference) reference); + dexPool.fieldSection.intern((FieldReference) reference); break; case ReferenceType.METHOD: - methodPool.intern((MethodReference)reference); + dexPool.methodSection.intern((MethodReference)reference); break; default: throw new ExceptionWithContext("Unrecognized reference type: %d", @@ -172,7 +154,7 @@ public class ClassPool implements ClassSection<CharSequence, CharSequence, for (TryBlock<? extends ExceptionHandler> tryBlock: methodImpl.getTryBlocks()) { for (ExceptionHandler handler: tryBlock.getExceptionHandlers()) { - typePool.internNullable(handler.getExceptionType()); + dexPool.typeSection.internNullable(handler.getExceptionType()); } } } @@ -182,7 +164,7 @@ public class ClassPool implements ClassSection<CharSequence, CharSequence, for (MethodParameter param: method.getParameters()) { String paramName = param.getName(); if (paramName != null) { - stringPool.intern(paramName); + dexPool.stringSection.intern(paramName); } } @@ -192,12 +174,12 @@ public class ClassPool implements ClassSection<CharSequence, CharSequence, switch (debugItem.getDebugItemType()) { case DebugItemType.START_LOCAL: StartLocal startLocal = (StartLocal)debugItem; - stringPool.internNullable(startLocal.getName()); - typePool.internNullable(startLocal.getType()); - stringPool.internNullable(startLocal.getSignature()); + dexPool.stringSection.internNullable(startLocal.getName()); + dexPool.typeSection.internNullable(startLocal.getType()); + dexPool.stringSection.internNullable(startLocal.getSignature()); break; case DebugItemType.SET_SOURCE_FILE: - stringPool.internNullable(((SetSourceFile) debugItem).getSourceFile()); + dexPool.stringSection.internNullable(((SetSourceFile) debugItem).getSourceFile()); break; } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/DexPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/DexPool.java index d12457a2..6d662ec3 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/DexPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/DexPool.java @@ -39,7 +39,7 @@ import org.jf.dexlib2.iface.ClassDef; import org.jf.dexlib2.iface.Field; import org.jf.dexlib2.iface.reference.*; import org.jf.dexlib2.iface.value.*; -import org.jf.dexlib2.writer.DexWriter; +import org.jf.dexlib2.writer.*; import org.jf.dexlib2.writer.io.DexDataStore; import org.jf.dexlib2.writer.io.FileDataStore; import org.jf.util.ExceptionWithContext; @@ -54,59 +54,69 @@ public class DexPool extends DexWriter<CharSequence, StringReference, CharSequen MethodProtoReference, FieldReference, MethodReference, PoolClassDef, Annotation, Set<? extends Annotation>, TypeListPool.Key<? extends Collection<? extends CharSequence>>, Field, PoolMethod, - EncodedValue, AnnotationElement> { + EncodedValue, AnnotationElement, StringPool, TypePool, ProtoPool, FieldPool, MethodPool, ClassPool, + TypeListPool, AnnotationPool, AnnotationSetPool> { - @Nonnull - public static DexPool makeDexPool() { - return makeDexPool(Opcodes.forApi(20)); - } - - @Deprecated - @Nonnull - public static DexPool makeDexPool(int api) { - return makeDexPool(Opcodes.forApi(api)); - } + private final Markable[] sections = new Markable[] { + stringSection, typeSection, protoSection, fieldSection, methodSection, classSection, typeListSection, + annotationSection, annotationSetSection + }; - @Nonnull - public static DexPool makeDexPool(@Nonnull Opcodes opcodes) { - StringPool stringPool = new StringPool(); - TypePool typePool = new TypePool(stringPool); - FieldPool fieldPool = new FieldPool(stringPool, typePool); - TypeListPool typeListPool = new TypeListPool(typePool); - ProtoPool protoPool = new ProtoPool(stringPool, typePool, typeListPool); - MethodPool methodPool = new MethodPool(stringPool, typePool, protoPool); - AnnotationPool annotationPool = new AnnotationPool(stringPool, typePool, fieldPool, methodPool); - AnnotationSetPool annotationSetPool = new AnnotationSetPool(annotationPool); - ClassPool classPool = new ClassPool(stringPool, typePool, fieldPool, methodPool, annotationSetPool, - typeListPool); - - return new DexPool(opcodes, stringPool, typePool, protoPool, fieldPool, methodPool, classPool, typeListPool, - annotationPool, annotationSetPool); + public DexPool(Opcodes opcodes) { + super(opcodes); } - private DexPool(Opcodes opcodes, StringPool stringPool, TypePool typePool, ProtoPool protoPool, FieldPool fieldPool, - MethodPool methodPool, ClassPool classPool, TypeListPool typeListPool, - AnnotationPool annotationPool, AnnotationSetPool annotationSetPool) { - super(opcodes, stringPool, typePool, protoPool, fieldPool, methodPool, - classPool, typeListPool, annotationPool, annotationSetPool); + @Nonnull @Override protected SectionProvider getSectionProvider() { + return new DexPoolSectionProvider(); } - public static void writeTo(@Nonnull DexDataStore dataStore, @Nonnull org.jf.dexlib2.iface.DexFile input) throws IOException { - DexPool dexPool = makeDexPool(); + public static void writeTo(@Nonnull DexDataStore dataStore, @Nonnull org.jf.dexlib2.iface.DexFile input) + throws IOException { + DexPool dexPool = new DexPool(input.getOpcodes()); for (ClassDef classDef: input.getClasses()) { - ((ClassPool)dexPool.classSection).intern(classDef); + dexPool.internClass(classDef); } dexPool.writeTo(dataStore); } public static void writeTo(@Nonnull String path, @Nonnull org.jf.dexlib2.iface.DexFile input) throws IOException { - DexPool dexPool = makeDexPool(); + DexPool dexPool = new DexPool(input.getOpcodes()); for (ClassDef classDef: input.getClasses()) { - ((ClassPool)dexPool.classSection).intern(classDef); + dexPool.internClass(classDef); } dexPool.writeTo(new FileDataStore(new File(path))); } + /** + * Interns a class into this DexPool + * @param classDef The class to intern + */ + public void internClass(ClassDef classDef) { + classSection.intern(classDef); + } + + /** + * Creates a marked state that can be returned to by calling reset() + * + * This is useful to rollback the last added class if it causes a method/field/type overflow + */ + public void mark() { + for (Markable section: sections) { + section.mark(); + } + } + + /** + * Resets to the last marked state + * + * This is useful to rollback the last added class if it causes a method/field/type overflow + */ + public void reset() { + for (Markable section: sections) { + section.reset(); + } + } + @Override protected void writeEncodedValue(@Nonnull InternalEncodedValueWriter writer, @Nonnull EncodedValue encodedValue) throws IOException { switch (encodedValue.getValueType()) { @@ -165,40 +175,74 @@ public class DexPool extends DexWriter<CharSequence, StringReference, CharSequen } } - public static void internEncodedValue(@Nonnull EncodedValue encodedValue, - @Nonnull StringPool stringPool, - @Nonnull TypePool typePool, - @Nonnull FieldPool fieldPool, - @Nonnull MethodPool methodPool) { + void internEncodedValue(@Nonnull EncodedValue encodedValue) { switch (encodedValue.getValueType()) { case ValueType.ANNOTATION: AnnotationEncodedValue annotationEncodedValue = (AnnotationEncodedValue)encodedValue; - typePool.intern(annotationEncodedValue.getType()); + typeSection.intern(annotationEncodedValue.getType()); for (AnnotationElement element: annotationEncodedValue.getElements()) { - stringPool.intern(element.getName()); - internEncodedValue(element.getValue(), stringPool, typePool, fieldPool, methodPool); + stringSection.intern(element.getName()); + internEncodedValue(element.getValue()); } break; case ValueType.ARRAY: for (EncodedValue element: ((ArrayEncodedValue)encodedValue).getValue()) { - internEncodedValue(element, stringPool, typePool, fieldPool, methodPool); + internEncodedValue(element); } break; case ValueType.STRING: - stringPool.intern(((StringEncodedValue)encodedValue).getValue()); + stringSection.intern(((StringEncodedValue)encodedValue).getValue()); break; case ValueType.TYPE: - typePool.intern(((TypeEncodedValue)encodedValue).getValue()); + typeSection.intern(((TypeEncodedValue)encodedValue).getValue()); break; case ValueType.ENUM: - fieldPool.intern(((EnumEncodedValue)encodedValue).getValue()); + fieldSection.intern(((EnumEncodedValue)encodedValue).getValue()); break; case ValueType.FIELD: - fieldPool.intern(((FieldEncodedValue)encodedValue).getValue()); + fieldSection.intern(((FieldEncodedValue)encodedValue).getValue()); break; case ValueType.METHOD: - methodPool.intern(((MethodEncodedValue)encodedValue).getValue()); + methodSection.intern(((MethodEncodedValue)encodedValue).getValue()); break; } } + + protected class DexPoolSectionProvider extends SectionProvider { + @Nonnull @Override public StringPool getStringSection() { + return new StringPool(DexPool.this); + } + + @Nonnull @Override public TypePool getTypeSection() { + return new TypePool(DexPool.this); + } + + @Nonnull @Override public ProtoPool getProtoSection() { + return new ProtoPool(DexPool.this); + } + + @Nonnull @Override public FieldPool getFieldSection() { + return new FieldPool(DexPool.this); + } + + @Nonnull @Override public MethodPool getMethodSection() { + return new MethodPool(DexPool.this); + } + + @Nonnull @Override public ClassPool getClassSection() { + return new ClassPool(DexPool.this); + } + + @Nonnull @Override public TypeListPool getTypeListSection() { + return new TypeListPool(DexPool.this); + } + + @Nonnull @Override public AnnotationPool getAnnotationSection() { + return new AnnotationPool(DexPool.this); + } + + @Nonnull @Override public AnnotationSetPool getAnnotationSetSection() { + return new AnnotationSetPool(DexPool.this); + } + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/FieldPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/FieldPool.java index 3403d12e..135d79bd 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/FieldPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/FieldPool.java @@ -39,20 +39,17 @@ import javax.annotation.Nonnull; public class FieldPool extends BaseIndexPool<FieldReference> implements FieldSection<CharSequence, CharSequence, FieldReference, Field> { - @Nonnull private final StringPool stringPool; - @Nonnull private final TypePool typePool; - public FieldPool(@Nonnull StringPool stringPool, @Nonnull TypePool typePool) { - this.stringPool = stringPool; - this.typePool = typePool; + public FieldPool(@Nonnull DexPool dexPool) { + super(dexPool); } public void intern(@Nonnull FieldReference field) { Integer prev = internedItems.put(field, 0); if (prev == null) { - typePool.intern(field.getDefiningClass()); - stringPool.intern(field.getName()); - typePool.intern(field.getType()); + dexPool.typeSection.intern(field.getDefiningClass()); + dexPool.stringSection.intern(field.getName()); + dexPool.typeSection.intern(field.getType()); } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/Markable.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/Markable.java new file mode 100644 index 00000000..8b14574c --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/Markable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2.writer.pool; + +public interface Markable { + void mark(); + void reset(); +} diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/MethodPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/MethodPool.java index 8103d319..2801abd0 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/MethodPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/MethodPool.java @@ -39,26 +39,24 @@ import javax.annotation.Nonnull; public class MethodPool extends BaseIndexPool<MethodReference> implements MethodSection<CharSequence, CharSequence, MethodProtoReference, MethodReference, PoolMethod> { - @Nonnull private final StringPool stringPool; - @Nonnull private final TypePool typePool; - @Nonnull private final ProtoPool protoPool; - public MethodPool(@Nonnull StringPool stringPool, @Nonnull TypePool typePool, - @Nonnull ProtoPool protoPool) { - this.stringPool = stringPool; - this.typePool = typePool; - this.protoPool = protoPool; + public MethodPool(@Nonnull DexPool dexPool) { + super(dexPool); } public void intern(@Nonnull MethodReference method) { Integer prev = internedItems.put(method, 0); if (prev == null) { - typePool.intern(method.getDefiningClass()); - protoPool.intern(new PoolMethodProto(method)); - stringPool.intern(method.getName()); + dexPool.typeSection.intern(method.getDefiningClass()); + dexPool.protoSection.intern(new PoolMethodProto(method)); + dexPool.stringSection.intern(method.getName()); } } + @Nonnull @Override public MethodReference getMethodReference(@Nonnull PoolMethod poolMethod) { + return poolMethod; + } + @Nonnull @Override public CharSequence getDefiningClass(@Nonnull MethodReference methodReference) { return methodReference.getDefiningClass(); } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ProtoPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ProtoPool.java index 523e5f4d..1209bd93 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ProtoPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ProtoPool.java @@ -43,23 +43,17 @@ import java.util.List; public class ProtoPool extends BaseIndexPool<MethodProtoReference> implements ProtoSection<CharSequence, CharSequence, MethodProtoReference, TypeListPool.Key<? extends Collection<? extends CharSequence>>> { - @Nonnull private final StringPool stringPool; - @Nonnull private final TypePool typePool; - @Nonnull private final TypeListPool typeListPool; - public ProtoPool(@Nonnull StringPool stringPool, @Nonnull TypePool typePool, - @Nonnull TypeListPool typeListPool) { - this.stringPool = stringPool; - this.typePool = typePool; - this.typeListPool = typeListPool; + public ProtoPool(@Nonnull DexPool dexPool) { + super(dexPool); } public void intern(@Nonnull MethodProtoReference reference) { Integer prev = internedItems.put(reference, 0); if (prev == null) { - stringPool.intern(getShorty(reference)); - typePool.intern(reference.getReturnType()); - typeListPool.intern(reference.getParameterTypes()); + dexPool.stringSection.intern(getShorty(reference)); + dexPool.typeSection.intern(reference.getReturnType()); + dexPool.typeListSection.intern(reference.getParameterTypes()); } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringPool.java index 5886b4f6..61f1502e 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringPool.java @@ -39,6 +39,11 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; public class StringPool extends StringTypeBasePool implements StringSection<CharSequence, StringReference> { + + public StringPool(@Nonnull DexPool dexPool) { + super(dexPool); + } + public void intern(@Nonnull CharSequence string) { internedItems.put(string.toString(), 0); } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringTypeBasePool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringTypeBasePool.java index 768e562d..54c6cea6 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringTypeBasePool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringTypeBasePool.java @@ -31,7 +31,6 @@ package org.jf.dexlib2.writer.pool; -import com.google.common.collect.Maps; import org.jf.dexlib2.writer.DexWriter; import org.jf.dexlib2.writer.NullableIndexSection; import org.jf.util.ExceptionWithContext; @@ -41,8 +40,12 @@ import javax.annotation.Nullable; import java.util.Collection; import java.util.Map; -public abstract class StringTypeBasePool implements NullableIndexSection<CharSequence> { - @Nonnull protected final Map<String, Integer> internedItems = Maps.newHashMap(); +public abstract class StringTypeBasePool extends BasePool<String, Integer> + implements NullableIndexSection<CharSequence>, Markable { + + public StringTypeBasePool(@Nonnull DexPool dexPool) { + super(dexPool); + } @Nonnull @Override public Collection<Map.Entry<String, Integer>> getItems() { return internedItems.entrySet(); diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypeListPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypeListPool.java index 7e0fbe03..038f4d1a 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypeListPool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypeListPool.java @@ -43,10 +43,10 @@ import java.util.Iterator; public class TypeListPool extends BaseNullableOffsetPool<Key<? extends Collection<? extends CharSequence>>> implements TypeListSection<CharSequence, Key<? extends Collection<? extends CharSequence>>> { - @Nonnull private final TypePool typePool; - public TypeListPool(@Nonnull TypePool typePool) { - this.typePool = typePool; + + public TypeListPool(@Nonnull DexPool dexPool) { + super(dexPool); } public void intern(@Nonnull Collection<? extends CharSequence> types) { @@ -55,7 +55,7 @@ public class TypeListPool extends BaseNullableOffsetPool<Key<? extends Collectio Integer prev = internedItems.put(key, 0); if (prev == null) { for (CharSequence type: types) { - typePool.intern(type); + dexPool.typeSection.intern(type); } } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypePool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypePool.java index 13bcd8ad..7e432089 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypePool.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypePool.java @@ -39,17 +39,17 @@ import javax.annotation.Nullable; public class TypePool extends StringTypeBasePool implements TypeSection<CharSequence, CharSequence, TypeReference> { - @Nonnull private final StringPool stringPool; - public TypePool(@Nonnull StringPool stringPool) { - this.stringPool = stringPool; + + public TypePool(@Nonnull DexPool dexPool) { + super(dexPool); } public void intern(@Nonnull CharSequence type) { String typeString = type.toString(); Integer prev = internedItems.put(typeString, 0); if (prev == null) { - stringPool.intern(typeString); + dexPool.stringSection.intern(typeString); } } diff --git a/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java b/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java index 4c8f85bf..ff832c27 100644 --- a/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java +++ b/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java @@ -79,7 +79,7 @@ public class AccessorTest { public void testAccessors() throws IOException { URL url = AccessorTest.class.getClassLoader().getResource("accessorTest.dex"); Assert.assertNotNull(url); - DexFile f = DexFileFactory.loadDexFile(url.getFile(), 15, false); + DexFile f = DexFileFactory.loadDexFile(url.getFile(), Opcodes.getDefault()); SyntheticAccessorResolver sar = new SyntheticAccessorResolver(f.getOpcodes(), f.getClasses()); diff --git a/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java b/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java new file mode 100644 index 00000000..610d3c76 --- /dev/null +++ b/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java @@ -0,0 +1,231 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2; + +import com.beust.jcommander.internal.Maps; +import com.google.common.collect.Lists; +import org.jf.dexlib2.DexFileFactory.DexEntryFinder; +import org.jf.dexlib2.DexFileFactory.DexFileNotFoundException; +import org.jf.dexlib2.DexFileFactory.MultipleMatchingDexEntriesException; +import org.jf.dexlib2.DexFileFactory.UnsupportedFileTypeException; +import org.jf.dexlib2.dexbacked.DexBackedDexFile; +import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile; +import org.jf.dexlib2.iface.MultiDexContainer; +import org.junit.Assert; +import org.junit.Test; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import static org.mockito.Mockito.mock; + +public class DexEntryFinderTest { + + @Test + public void testNormalStuff() throws Exception { + Map<String, DexBackedDexFile> entries = Maps.newHashMap(); + DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class); + entries.put("/system/framework/framework.jar", dexFile1); + DexBackedDexFile dexFile2 = mock(DexBackedDexFile.class); + entries.put("/system/framework/framework.jar:classes2.dex", dexFile2); + DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries)); + + Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true)); + + assertEntryNotFound(testFinder, "system/framework/framework.jar", true); + assertEntryNotFound(testFinder, "/framework/framework.jar", true); + assertEntryNotFound(testFinder, "framework/framework.jar", true); + assertEntryNotFound(testFinder, "/framework.jar", true); + assertEntryNotFound(testFinder, "framework.jar", true); + + Assert.assertEquals(dexFile1, testFinder.findEntry("system/framework/framework.jar", false)); + Assert.assertEquals(dexFile1, testFinder.findEntry("/framework/framework.jar", false)); + Assert.assertEquals(dexFile1, testFinder.findEntry("framework/framework.jar", false)); + Assert.assertEquals(dexFile1, testFinder.findEntry("/framework.jar", false)); + Assert.assertEquals(dexFile1, testFinder.findEntry("framework.jar", false)); + + assertEntryNotFound(testFinder, "ystem/framework/framework.jar", false); + assertEntryNotFound(testFinder, "ssystem/framework/framework.jar", false); + assertEntryNotFound(testFinder, "ramework/framework.jar", false); + assertEntryNotFound(testFinder, "ramework.jar", false); + assertEntryNotFound(testFinder, "framework", false); + + Assert.assertEquals(dexFile2, testFinder.findEntry("/system/framework/framework.jar:classes2.dex", true)); + + assertEntryNotFound(testFinder, "system/framework/framework.jar:classes2.dex", true); + assertEntryNotFound(testFinder, "framework.jar:classes2.dex", true); + assertEntryNotFound(testFinder, "classes2.dex", true); + + Assert.assertEquals(dexFile2, testFinder.findEntry("system/framework/framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("framework/framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("/framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("framework.jar:classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry(":classes2.dex", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("classes2.dex", false)); + + assertEntryNotFound(testFinder, "ystem/framework/framework.jar:classes2.dex", false); + assertEntryNotFound(testFinder, "ramework.jar:classes2.dex", false); + assertEntryNotFound(testFinder, "lasses2.dex", false); + assertEntryNotFound(testFinder, "classes2", false); + } + + @Test + public void testSimilarEntries() throws Exception { + Map<String, DexBackedDexFile> entries = Maps.newHashMap(); + DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class); + entries.put("/system/framework/framework.jar", dexFile1); + DexBackedDexFile dexFile2 = mock(DexBackedDexFile.class); + entries.put("system/framework/framework.jar", dexFile2); + DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries)); + + Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true)); + Assert.assertEquals(dexFile2, testFinder.findEntry("system/framework/framework.jar", true)); + + assertMultipleMatchingEntries(testFinder, "/system/framework/framework.jar"); + assertMultipleMatchingEntries(testFinder, "system/framework/framework.jar"); + + assertMultipleMatchingEntries(testFinder, "/framework/framework.jar"); + assertMultipleMatchingEntries(testFinder, "framework/framework.jar"); + assertMultipleMatchingEntries(testFinder, "/framework.jar"); + assertMultipleMatchingEntries(testFinder, "framework.jar"); + } + + @Test + public void testMatchingSuffix() throws Exception { + Map<String, DexBackedDexFile> entries = Maps.newHashMap(); + DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class); + entries.put("/system/framework/framework.jar", dexFile1); + DexBackedDexFile dexFile2 = mock(DexBackedDexFile.class); + entries.put("/framework/framework.jar", dexFile2); + DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries)); + + Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true)); + Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar", true)); + + Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar", false)); + Assert.assertEquals(dexFile2, testFinder.findEntry("framework/framework.jar", false)); + + assertMultipleMatchingEntries(testFinder, "/framework.jar"); + assertMultipleMatchingEntries(testFinder, "framework.jar"); + } + + @Test + public void testNonDexEntries() throws Exception { + Map<String, DexBackedDexFile> entries = Maps.newHashMap(); + DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class); + entries.put("classes.dex", dexFile1); + entries.put("/blah/classes.dex", null); + DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries)); + + Assert.assertEquals(dexFile1, testFinder.findEntry("classes.dex", true)); + Assert.assertEquals(dexFile1, testFinder.findEntry("classes.dex", false)); + + assertUnsupportedFileType(testFinder, "/blah/classes.dex", true); + assertDexFileNotFound(testFinder, "/blah/classes.dex", false); + } + + private void assertEntryNotFound(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException { + try { + finder.findEntry(entry, exactMatch); + Assert.fail(); + } catch (DexFileNotFoundException ex) { + // expected exception + } + } + + private void assertMultipleMatchingEntries(DexEntryFinder finder, String entry) throws IOException { + try { + finder.findEntry(entry, false); + Assert.fail(); + } catch (MultipleMatchingDexEntriesException ex) { + // expected exception + } + } + + private void assertUnsupportedFileType(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException { + try { + finder.findEntry(entry, exactMatch); + Assert.fail(); + } catch (UnsupportedFileTypeException ex) { + // expected exception + } + } + + private void assertDexFileNotFound(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException { + try { + finder.findEntry(entry, exactMatch); + Assert.fail(); + } catch (DexFileNotFoundException ex) { + // expected exception + } + } + + public static class TestMultiDexContainer implements MultiDexContainer<DexBackedDexFile> { + @Nonnull private final Map<String, DexBackedDexFile> entries; + + public TestMultiDexContainer(@Nonnull Map<String, DexBackedDexFile> entries) { + this.entries = entries; + } + + @Nonnull @Override public List<String> getDexEntryNames() throws IOException { + List<String> entryNames = Lists.newArrayList(); + + for (Entry<String, DexBackedDexFile> entry: entries.entrySet()) { + if (entry.getValue() != null) { + entryNames.add(entry.getKey()); + } + } + + return entryNames; + } + + @Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException { + if (entries.containsKey(entryName)) { + DexBackedDexFile entry = entries.get(entryName); + if (entry == null) { + throw new NotADexFile(); + } + return entry; + } + return null; + } + + @Nonnull @Override public Opcodes getOpcodes() { + return Opcodes.getDefault(); + } + } +} diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CommonSuperclassTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CommonSuperclassTest.java index 3f1ee56d..d69dd81a 100644 --- a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CommonSuperclassTest.java +++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CommonSuperclassTest.java @@ -32,8 +32,10 @@ package org.jf.dexlib2.analysis; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; import junit.framework.Assert; import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.iface.ClassDef; import org.jf.dexlib2.immutable.ImmutableDexFile; import org.junit.Test; @@ -51,49 +53,53 @@ public class CommonSuperclassTest { // fivetwothree // fivethree - private final ClassPath classPath; + private final ClassPath oldClassPath; + private final ClassPath newClassPath; + public CommonSuperclassTest() throws IOException { - classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.forApi(19), - ImmutableSet.of( - TestUtils.makeClassDef("Ljava/lang/Object;", null), - TestUtils.makeClassDef("Ltest/one;", "Ljava/lang/Object;"), - TestUtils.makeClassDef("Ltest/two;", "Ljava/lang/Object;"), - TestUtils.makeClassDef("Ltest/onetwo;", "Ltest/one;"), - TestUtils.makeClassDef("Ltest/onetwothree;", "Ltest/onetwo;"), - TestUtils.makeClassDef("Ltest/onethree;", "Ltest/one;"), - TestUtils.makeClassDef("Ltest/fivetwo;", "Ltest/five;"), - TestUtils.makeClassDef("Ltest/fivetwothree;", "Ltest/fivetwo;"), - TestUtils.makeClassDef("Ltest/fivethree;", "Ltest/five;"), - TestUtils.makeInterfaceDef("Ljava/lang/Cloneable;"), - TestUtils.makeInterfaceDef("Ljava/io/Serializable;"), - - // basic class and interface - TestUtils.makeClassDef("Liface/classiface1;", "Ljava/lang/Object;", "Liface/iface1;"), - TestUtils.makeInterfaceDef("Liface/iface1;"), - - // a more complex interface tree - TestUtils.makeInterfaceDef("Liface/base1;"), - // implements undefined interface - TestUtils.makeInterfaceDef("Liface/sub1;", "Liface/base1;", "Liface/base2;"), - // this implements sub1, so that its interfaces can't be fully resolved either - TestUtils.makeInterfaceDef("Liface/sub2;", "Liface/base1;", "Liface/sub1;"), - TestUtils.makeInterfaceDef("Liface/sub3;", "Liface/base1;"), - TestUtils.makeInterfaceDef("Liface/sub4;", "Liface/base1;", "Liface/sub3;"), - TestUtils.makeClassDef("Liface/classsub1;", "Ljava/lang/Object;", "Liface/sub1;"), - TestUtils.makeClassDef("Liface/classsub2;", "Ljava/lang/Object;", "Liface/sub2;"), - TestUtils.makeClassDef("Liface/classsub3;", "Ljava/lang/Object;", "Liface/sub3;", - "Liface/base;"), - TestUtils.makeClassDef("Liface/classsub4;", "Ljava/lang/Object;", "Liface/sub3;", - "Liface/sub4;"), - TestUtils.makeClassDef("Liface/classsubsub4;", "Liface/classsub4;"), - TestUtils.makeClassDef("Liface/classsub1234;", "Ljava/lang/Object;", "Liface/sub1;", - "Liface/sub2;", "Liface/sub3;", "Liface/sub4;") - )))); + ImmutableSet<ClassDef> classes = ImmutableSet.of( + TestUtils.makeClassDef("Ljava/lang/Object;", null), + TestUtils.makeClassDef("Ltest/one;", "Ljava/lang/Object;"), + TestUtils.makeClassDef("Ltest/two;", "Ljava/lang/Object;"), + TestUtils.makeClassDef("Ltest/onetwo;", "Ltest/one;"), + TestUtils.makeClassDef("Ltest/onetwothree;", "Ltest/onetwo;"), + TestUtils.makeClassDef("Ltest/onethree;", "Ltest/one;"), + TestUtils.makeClassDef("Ltest/fivetwo;", "Ltest/five;"), + TestUtils.makeClassDef("Ltest/fivetwothree;", "Ltest/fivetwo;"), + TestUtils.makeClassDef("Ltest/fivethree;", "Ltest/five;"), + TestUtils.makeInterfaceDef("Ljava/lang/Cloneable;"), + TestUtils.makeInterfaceDef("Ljava/io/Serializable;"), + + // basic class and interface + TestUtils.makeClassDef("Liface/classiface1;", "Ljava/lang/Object;", "Liface/iface1;"), + TestUtils.makeInterfaceDef("Liface/iface1;"), + + // a more complex interface tree + TestUtils.makeInterfaceDef("Liface/base1;"), + // implements undefined interface + TestUtils.makeInterfaceDef("Liface/sub1;", "Liface/base1;", "Liface/base2;"), + // this implements sub1, so that its interfaces can't be fully resolved either + TestUtils.makeInterfaceDef("Liface/sub2;", "Liface/base1;", "Liface/sub1;"), + TestUtils.makeInterfaceDef("Liface/sub3;", "Liface/base1;"), + TestUtils.makeInterfaceDef("Liface/sub4;", "Liface/base1;", "Liface/sub3;"), + TestUtils.makeClassDef("Liface/classsub1;", "Ljava/lang/Object;", "Liface/sub1;"), + TestUtils.makeClassDef("Liface/classsub2;", "Ljava/lang/Object;", "Liface/sub2;"), + TestUtils.makeClassDef("Liface/classsub3;", "Ljava/lang/Object;", "Liface/sub3;", + "Liface/base;"), + TestUtils.makeClassDef("Liface/classsub4;", "Ljava/lang/Object;", "Liface/sub3;", + "Liface/sub4;"), + TestUtils.makeClassDef("Liface/classsubsub4;", "Liface/classsub4;"), + TestUtils.makeClassDef("Liface/classsub1234;", "Ljava/lang/Object;", "Liface/sub1;", + "Liface/sub2;", "Liface/sub3;", "Liface/sub4;")); + + oldClassPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.getDefault(), classes))); + newClassPath = new ClassPath(Lists.newArrayList(new DexClassProvider( + new ImmutableDexFile(Opcodes.forArtVersion(72), classes))), true, 72); } - public void superclassTest(String commonSuperclass, - String type1, String type2) { + public void superclassTest(ClassPath classPath, String commonSuperclass, + String type1, String type2) { TypeProto commonSuperclassProto = classPath.getClass(commonSuperclass); TypeProto type1Proto = classPath.getClass(type1); TypeProto type2Proto = classPath.getClass(type2); @@ -102,6 +108,11 @@ public class CommonSuperclassTest { Assert.assertSame(commonSuperclassProto, type2Proto.getCommonSuperclass(type1Proto)); } + public void superclassTest(String commonSuperclass, String type1, String type2) { + superclassTest(oldClassPath, commonSuperclass, type1, type2); + superclassTest(newClassPath, commonSuperclass, type1, type2); + } + @Test public void testGetCommonSuperclass() throws IOException { String object = "Ljava/lang/Object;"; @@ -131,7 +142,11 @@ public class CommonSuperclassTest { // same value, but different object Assert.assertEquals( onetwo, - classPath.getClass(onetwo).getCommonSuperclass(new ClassProto(classPath, onetwo)).getType()); + oldClassPath.getClass(onetwo).getCommonSuperclass(new ClassProto(oldClassPath, onetwo)).getType()); + + Assert.assertEquals( + onetwo, + newClassPath.getClass(onetwo).getCommonSuperclass(new ClassProto(newClassPath, onetwo)).getType()); // other object is superclass superclassTest(object, object, one); diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java index 90a63590..70e6a042 100644 --- a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java +++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java @@ -51,11 +51,12 @@ import org.jf.dexlib2.immutable.instruction.ImmutableInstruction35mi; import org.junit.Assert; import org.junit.Test; +import java.io.IOException; import java.util.List; public class CustomMethodInlineTableTest { @Test - public void testCustomMethodInlineTable_Virtual() { + public void testCustomMethodInlineTable_Virtual() throws IOException { List<ImmutableInstruction> instructions = Lists.newArrayList( new ImmutableInstruction35mi(Opcode.EXECUTE_INLINE, 1, 0, 0, 0, 0, 0, 0), new ImmutableInstruction10x(Opcode.RETURN_VOID)); @@ -67,10 +68,12 @@ public class CustomMethodInlineTableTest { ClassDef classDef = new ImmutableClassDef("Lblah;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, null, null, null, null, null, ImmutableList.of(method)); - DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef)); + DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), ImmutableList.of(classDef)); + + ClassPathResolver resolver = new ClassPathResolver(ImmutableList.<String>of(), + ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile); + ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART); - ClassPath classPath = ClassPath.fromClassPath(ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile, - 15, false); InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V"); MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false); @@ -82,7 +85,7 @@ public class CustomMethodInlineTableTest { } @Test - public void testCustomMethodInlineTable_Static() { + public void testCustomMethodInlineTable_Static() throws IOException { List<ImmutableInstruction> instructions = Lists.newArrayList( new ImmutableInstruction35mi(Opcode.EXECUTE_INLINE, 1, 0, 0, 0, 0, 0, 0), new ImmutableInstruction10x(Opcode.RETURN_VOID)); @@ -94,10 +97,12 @@ public class CustomMethodInlineTableTest { ClassDef classDef = new ImmutableClassDef("Lblah;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, null, null, null, null, ImmutableList.of(method), null); - DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef)); + DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), ImmutableList.of(classDef)); + + ClassPathResolver resolver = new ClassPathResolver(ImmutableList.<String>of(), + ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile); + ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART); - ClassPath classPath = ClassPath.fromClassPath(ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile, - 15, false); InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V"); MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false); @@ -109,7 +114,7 @@ public class CustomMethodInlineTableTest { } @Test - public void testCustomMethodInlineTable_Direct() { + public void testCustomMethodInlineTable_Direct() throws IOException { List<ImmutableInstruction> instructions = Lists.newArrayList( new ImmutableInstruction35mi(Opcode.EXECUTE_INLINE, 1, 0, 0, 0, 0, 0, 0), new ImmutableInstruction10x(Opcode.RETURN_VOID)); @@ -121,10 +126,12 @@ public class CustomMethodInlineTableTest { ClassDef classDef = new ImmutableClassDef("Lblah;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, null, null, null, null, ImmutableList.of(method), null); - DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef)); + DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), ImmutableList.of(classDef)); + + ClassPathResolver resolver = new ClassPathResolver(ImmutableList.<String>of(), + ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile); + ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART); - ClassPath classPath = ClassPath.fromClassPath(ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile, - 15, false); InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V"); MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false); diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/MethodAnalyzerTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/MethodAnalyzerTest.java new file mode 100644 index 00000000..215ba179 --- /dev/null +++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/MethodAnalyzerTest.java @@ -0,0 +1,260 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2.analysis; + +import com.google.common.collect.Lists; +import org.jf.dexlib2.AccessFlags; +import org.jf.dexlib2.Opcode; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.builder.MethodImplementationBuilder; +import org.jf.dexlib2.builder.instruction.BuilderInstruction10x; +import org.jf.dexlib2.builder.instruction.BuilderInstruction12x; +import org.jf.dexlib2.builder.instruction.BuilderInstruction21t; +import org.jf.dexlib2.builder.instruction.BuilderInstruction22c; +import org.jf.dexlib2.iface.ClassDef; +import org.jf.dexlib2.iface.DexFile; +import org.jf.dexlib2.iface.Method; +import org.jf.dexlib2.iface.MethodImplementation; +import org.jf.dexlib2.immutable.ImmutableClassDef; +import org.jf.dexlib2.immutable.ImmutableDexFile; +import org.jf.dexlib2.immutable.ImmutableMethod; +import org.jf.dexlib2.immutable.ImmutableMethodParameter; +import org.jf.dexlib2.immutable.reference.ImmutableTypeReference; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.jf.dexlib2.Opcodes.forArtVersion; + +public class MethodAnalyzerTest { + + @Test + public void testInstanceOfNarrowingEqz_art() throws IOException { + MethodImplementationBuilder builder = new MethodImplementationBuilder(2); + + builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1, + new ImmutableTypeReference("Lmain;"))); + builder.addInstruction(new BuilderInstruction21t(Opcode.IF_EQZ, 0, builder.getLabel("not_instance_of"))); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + builder.addLabel("not_instance_of"); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + MethodImplementation methodImplementation = builder.getMethodImplementation(); + + Method method = new ImmutableMethod("Lmain;", "narrowing", + Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V", + AccessFlags.PUBLIC.getValue(), null, methodImplementation); + ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, + null, null, null, Collections.singletonList(method)); + DexFile dexFile = new ImmutableDexFile(forArtVersion(56), Collections.singletonList(classDef)); + + ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), true, 56); + MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false); + + List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions(); + Assert.assertEquals("Lmain;", analyzedInstructions.get(2).getPreInstructionRegisterType(1).type.getType()); + + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType()); + } + + @Test + public void testInstanceOfNarrowingEqz_dalvik() throws IOException { + MethodImplementationBuilder builder = new MethodImplementationBuilder(2); + + builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1, + new ImmutableTypeReference("Lmain;"))); + builder.addInstruction(new BuilderInstruction21t(Opcode.IF_EQZ, 0, builder.getLabel("not_instance_of"))); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + builder.addLabel("not_instance_of"); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + MethodImplementation methodImplementation = builder.getMethodImplementation(); + + Method method = new ImmutableMethod("Lmain;", "narrowing", + Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V", + AccessFlags.PUBLIC.getValue(), null, methodImplementation); + ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, + null, null, null, Collections.singletonList(method)); + DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), Collections.singletonList(classDef)); + + ClassPath classPath = new ClassPath(new DexClassProvider(dexFile)); + MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false); + + List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions(); + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(2).getPreInstructionRegisterType(1).type.getType()); + + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType()); + } + + @Test + public void testInstanceOfNarrowingNez_art() throws IOException { + MethodImplementationBuilder builder = new MethodImplementationBuilder(2); + + builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1, + new ImmutableTypeReference("Lmain;"))); + builder.addInstruction(new BuilderInstruction21t(Opcode.IF_NEZ, 0, builder.getLabel("instance_of"))); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + builder.addLabel("instance_of"); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + MethodImplementation methodImplementation = builder.getMethodImplementation(); + + Method method = new ImmutableMethod("Lmain;", "narrowing", + Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V", + AccessFlags.PUBLIC.getValue(), null, methodImplementation); + ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, + null, null, null, Collections.singletonList(method)); + DexFile dexFile = new ImmutableDexFile(forArtVersion(56), Collections.singletonList(classDef)); + + ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), true, 56); + MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false); + + List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions(); + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(2).getPreInstructionRegisterType(1).type.getType()); + + Assert.assertEquals("Lmain;", analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType()); + } + + @Test + public void testInstanceOfNarrowingNez_dalvik() throws IOException { + MethodImplementationBuilder builder = new MethodImplementationBuilder(2); + + builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1, + new ImmutableTypeReference("Lmain;"))); + builder.addInstruction(new BuilderInstruction21t(Opcode.IF_NEZ, 0, builder.getLabel("instance_of"))); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + builder.addLabel("instance_of"); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + MethodImplementation methodImplementation = builder.getMethodImplementation(); + + Method method = new ImmutableMethod("Lmain;", "narrowing", + Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V", + AccessFlags.PUBLIC.getValue(), null, methodImplementation); + ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, + null, null, null, Collections.singletonList(method)); + DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), Collections.singletonList(classDef)); + + ClassPath classPath = new ClassPath(new DexClassProvider(dexFile)); + MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false); + + List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions(); + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(2).getPreInstructionRegisterType(1).type.getType()); + + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType()); + } + + @Test + public void testInstanceOfNarrowingAfterMove_art() throws IOException { + MethodImplementationBuilder builder = new MethodImplementationBuilder(3); + + builder.addInstruction(new BuilderInstruction12x(Opcode.MOVE_OBJECT, 1, 2)); + builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1, + new ImmutableTypeReference("Lmain;"))); + builder.addInstruction(new BuilderInstruction21t(Opcode.IF_EQZ, 0, builder.getLabel("not_instance_of"))); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + builder.addLabel("not_instance_of"); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + MethodImplementation methodImplementation = builder.getMethodImplementation(); + + Method method = new ImmutableMethod("Lmain;", "narrowing", + Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V", + AccessFlags.PUBLIC.getValue(), null, methodImplementation); + ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, + null, null, null, Collections.singletonList(method)); + DexFile dexFile = new ImmutableDexFile(forArtVersion(56), Collections.singletonList(classDef)); + + ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), true, 56); + MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false); + + List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions(); + Assert.assertEquals("Lmain;", analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType()); + Assert.assertEquals("Lmain;", analyzedInstructions.get(3).getPreInstructionRegisterType(2).type.getType()); + + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(4).getPreInstructionRegisterType(1).type.getType()); + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(4).getPreInstructionRegisterType(2).type.getType()); + } + + @Test + public void testInstanceOfNarrowingAfterMove_dalvik() throws IOException { + MethodImplementationBuilder builder = new MethodImplementationBuilder(3); + + builder.addInstruction(new BuilderInstruction12x(Opcode.MOVE_OBJECT, 1, 2)); + builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1, + new ImmutableTypeReference("Lmain;"))); + builder.addInstruction(new BuilderInstruction21t(Opcode.IF_EQZ, 0, builder.getLabel("not_instance_of"))); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + builder.addLabel("not_instance_of"); + builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID)); + + MethodImplementation methodImplementation = builder.getMethodImplementation(); + + Method method = new ImmutableMethod("Lmain;", "narrowing", + Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V", + AccessFlags.PUBLIC.getValue(), null, methodImplementation); + ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, + null, null, null, Collections.singletonList(method)); + DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), Collections.singletonList(classDef)); + + ClassPath classPath = new ClassPath(new DexClassProvider(dexFile)); + MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false); + + List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions(); + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType()); + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(3).getPreInstructionRegisterType(2).type.getType()); + + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(4).getPreInstructionRegisterType(1).type.getType()); + Assert.assertEquals("Ljava/lang/Object;", + analyzedInstructions.get(4).getPreInstructionRegisterType(2).type.getType()); + } +} diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/util/SuperclassChainTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/util/SuperclassChainTest.java index 84cd284b..78bc8a51 100644 --- a/dexlib2/src/test/java/org/jf/dexlib2/analysis/util/SuperclassChainTest.java +++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/util/SuperclassChainTest.java @@ -57,7 +57,7 @@ public class SuperclassChainTest { ImmutableSet<ClassDef> classes = ImmutableSet.<ClassDef>of( objectClassDef, oneClassDef, twoClassDef, threeClassDef); - ClassPath classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.forApi(19), classes))); + ClassPath classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.getDefault(), classes))); TypeProto objectClassProto = classPath.getClass("Ljava/lang/Object;"); TypeProto oneClassProto = classPath.getClass("Ltest/one;"); @@ -88,7 +88,7 @@ public class SuperclassChainTest { ClassDef twoClassDef = TestUtils.makeClassDef("Ltest/two;", "Ltest/one;"); ClassDef threeClassDef = TestUtils.makeClassDef("Ltest/three;", "Ltest/two;"); ImmutableSet<ClassDef> classes = ImmutableSet.<ClassDef>of(twoClassDef, threeClassDef); - ClassPath classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.forApi(19), classes))); + ClassPath classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.getDefault(), classes))); TypeProto unknownClassProto = classPath.getUnknownClass(); TypeProto oneClassProto = classPath.getClass("Ltest/one;"); diff --git a/dexlib2/src/test/java/org/jf/dexlib2/pool/RollbackTest.java b/dexlib2/src/test/java/org/jf/dexlib2/pool/RollbackTest.java new file mode 100644 index 00000000..6074de14 --- /dev/null +++ b/dexlib2/src/test/java/org/jf/dexlib2/pool/RollbackTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.dexlib2.pool; + +import com.google.common.collect.Lists; +import org.jf.dexlib2.AccessFlags; +import org.jf.dexlib2.AnnotationVisibility; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.dexbacked.raw.MapItem; +import org.jf.dexlib2.dexbacked.raw.RawDexFile; +import org.jf.dexlib2.iface.ClassDef; +import org.jf.dexlib2.iface.Field; +import org.jf.dexlib2.iface.Method; +import org.jf.dexlib2.iface.MethodParameter; +import org.jf.dexlib2.immutable.*; +import org.jf.dexlib2.writer.io.MemoryDataStore; +import org.jf.dexlib2.writer.pool.DexPool; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.util.List; + +public class RollbackTest { + @Test + public void testRollback() throws IOException { + ClassDef class1 = new ImmutableClassDef("Lcls1;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, null, + Lists.newArrayList(new ImmutableAnnotation(AnnotationVisibility.RUNTIME, "Lannotation;", null)), + Lists.<Field>newArrayList( + new ImmutableField("Lcls1;", "field1", "I", AccessFlags.PUBLIC.getValue(), null, null) + ), + Lists.<Method>newArrayList( + new ImmutableMethod("Lcls1", "method1", + Lists.<MethodParameter>newArrayList(new ImmutableMethodParameter("L", null, null)), "V", + AccessFlags.PUBLIC.getValue(), null, null)) + ); + + ClassDef class2 = new ImmutableClassDef("Lcls2;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, null, + Lists.newArrayList(new ImmutableAnnotation(AnnotationVisibility.RUNTIME, "Lannotation2;", null)), + Lists.<Field>newArrayList( + new ImmutableField("Lcls2;", "field2", "D", AccessFlags.PUBLIC.getValue(), null, null) + ), + Lists.<Method>newArrayList( + new ImmutableMethod("Lcls2;", "method2", + Lists.<MethodParameter>newArrayList(new ImmutableMethodParameter("D", null, null)), "V", + AccessFlags.PUBLIC.getValue(), null, null)) + ); + + RawDexFile dexFile1; + { + MemoryDataStore dataStore = new MemoryDataStore(); + DexPool dexPool = new DexPool(Opcodes.getDefault()); + dexPool.internClass(class1); + dexPool.mark(); + dexPool.internClass(class2); + dexPool.reset(); + dexPool.writeTo(dataStore); + dexFile1 = new RawDexFile(Opcodes.getDefault(), dataStore.getData()); + } + + RawDexFile dexFile2; + { + MemoryDataStore dataStore = new MemoryDataStore(); + DexPool dexPool = new DexPool(Opcodes.getDefault()); + dexPool.internClass(class1); + dexPool.writeTo(dataStore); + dexFile2 = new RawDexFile(Opcodes.getDefault(), dataStore.getData()); + } + + List<MapItem> mapItems1 = dexFile1.getMapItems(); + List<MapItem> mapItems2 = dexFile2.getMapItems(); + for (int i=0; i<mapItems1.size(); i++) { + Assert.assertEquals(mapItems1.get(i).getType(), mapItems2.get(i).getType()); + Assert.assertEquals(mapItems1.get(i).getItemCount(), mapItems2.get(i).getItemCount()); + } + } +} diff --git a/dexlib2/src/test/java/org/jf/dexlib2/writer/DexWriterTest.java b/dexlib2/src/test/java/org/jf/dexlib2/writer/DexWriterTest.java index 1a0a2892..bf55e37f 100644 --- a/dexlib2/src/test/java/org/jf/dexlib2/writer/DexWriterTest.java +++ b/dexlib2/src/test/java/org/jf/dexlib2/writer/DexWriterTest.java @@ -72,12 +72,12 @@ public class DexWriterTest { MemoryDataStore dataStore = new MemoryDataStore(); try { - DexPool.writeTo(dataStore, new ImmutableDexFile(Opcodes.forApi(19), ImmutableSet.of(classDef))); + DexPool.writeTo(dataStore, new ImmutableDexFile(Opcodes.getDefault(), ImmutableSet.of(classDef))); } catch (IOException ex) { throw new RuntimeException(ex); } - DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(15), dataStore.getData()); + DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dataStore.getData()); ClassDef dbClassDef = Iterables.getFirst(dexFile.getClasses(), null); Assert.assertNotNull(dbClassDef); Annotation dbAnnotation = Iterables.getFirst(dbClassDef.getAnnotations(), null); @@ -112,12 +112,12 @@ public class DexWriterTest { MemoryDataStore dataStore = new MemoryDataStore(); try { - DexPool.writeTo(dataStore, new ImmutableDexFile(Opcodes.forApi(19), ImmutableSet.of(classDef))); + DexPool.writeTo(dataStore, new ImmutableDexFile(Opcodes.getDefault(), ImmutableSet.of(classDef))); } catch (IOException ex) { throw new RuntimeException(ex); } - DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(15), dataStore.getData()); + DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dataStore.getData()); ClassDef dbClassDef = Iterables.getFirst(dexFile.getClasses(), null); Assert.assertNotNull(dbClassDef); Annotation dbAnnotation = Iterables.getFirst(dbClassDef.getAnnotations(), null); diff --git a/dexlib2/src/test/java/org/jf/dexlib2/writer/JumboStringConversionTest.java b/dexlib2/src/test/java/org/jf/dexlib2/writer/JumboStringConversionTest.java index c246e0ec..340b1fa1 100644 --- a/dexlib2/src/test/java/org/jf/dexlib2/writer/JumboStringConversionTest.java +++ b/dexlib2/src/test/java/org/jf/dexlib2/writer/JumboStringConversionTest.java @@ -62,7 +62,7 @@ import java.util.List; public class JumboStringConversionTest { @Test public void testJumboStringConversion() throws IOException { - DexBuilder dexBuilder = DexBuilder.makeDexBuilder(Opcodes.forApi(15)); + DexBuilder dexBuilder = new DexBuilder(Opcodes.getDefault()); MethodImplementationBuilder methodBuilder = new MethodImplementationBuilder(1); for (int i=0; i<66000; i++) { @@ -92,7 +92,7 @@ public class JumboStringConversionTest { MemoryDataStore dexStore = new MemoryDataStore(); dexBuilder.writeTo(dexStore); - DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(15), dexStore.getData()); + DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dexStore.getData()); ClassDef classDef = Iterables.getFirst(dexFile.getClasses(), null); Assert.assertNotNull(classDef); @@ -122,7 +122,7 @@ public class JumboStringConversionTest { @Test public void testJumboStringConversion_NonMethodBuilder() throws IOException { - DexBuilder dexBuilder = DexBuilder.makeDexBuilder(Opcodes.forApi(15)); + DexBuilder dexBuilder = new DexBuilder(Opcodes.getDefault()); final List<Instruction> instructions = Lists.newArrayList(); for (int i=0; i<66000; i++) { @@ -189,7 +189,7 @@ public class JumboStringConversionTest { MemoryDataStore dexStore = new MemoryDataStore(); dexBuilder.writeTo(dexStore); - DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(15), dexStore.getData()); + DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dexStore.getData()); ClassDef classDef = Iterables.getFirst(dexFile.getClasses(), null); Assert.assertNotNull(classDef); diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differindex 5ccda13e..6ffa2378 100644 --- a/gradle/wrapper/gradle-wrapper.jar +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 23bc0f51..8ee9c631 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jul 08 16:46:58 PDT 2016 +#Wed Sep 28 23:22:26 AEST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -157,4 +161,9 @@ function splitJvmOpts() { eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then + cd "$(dirname "$0")" +fi + exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat index 72d362da..e95643d6 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -49,7 +49,6 @@ goto fail @rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
diff --git a/smali/build.gradle b/smali/build.gradle index 318b5a97..6472f216 100644 --- a/smali/build.gradle +++ b/smali/build.gradle @@ -76,8 +76,8 @@ dependencies { compile project(':util') compile project(':dexlib2') compile depends.antlr_runtime + compile depends.jcommander compile depends.stringtemplate - compile depends.commons_cli testCompile depends.junit @@ -95,7 +95,7 @@ task fatJar(type: Jar, dependsOn: jar) { classifier = 'fat' manifest { - attributes('Main-Class': 'org.jf.smali.main') + attributes('Main-Class': 'org.jf.smali.Main') } doLast { @@ -141,7 +141,8 @@ task proguard(type: proguard.gradle.ProGuardTask, dependsOn: fatJar) { dontobfuscate dontoptimize - keep 'public class org.jf.smali.main { public static void main(java.lang.String[]); }' + keep 'public class org.jf.smali.Main { public static void main(java.lang.String[]); }' + keep 'class com.beust.jcommander.** { *; }' keepclassmembers 'enum * { public static **[] values(); public static ** valueOf(java.lang.String); }' dontwarn 'com.google.common.**' diff --git a/smali/src/main/antlr/smaliParser.g b/smali/src/main/antlr/smaliParser.g index 29cd141b..2d5eccaa 100644 --- a/smali/src/main/antlr/smaliParser.g +++ b/smali/src/main/antlr/smaliParser.g @@ -263,8 +263,8 @@ import org.jf.dexlib2.Opcodes; this.allowOdex = allowOdex; } - public void setApiLevel(int apiLevel, boolean experimental) { - this.opcodes = new Opcodes(apiLevel, experimental); + public void setApiLevel(int apiLevel) { + this.opcodes = Opcodes.forApi(apiLevel); this.apiLevel = apiLevel; } diff --git a/smali/src/main/antlr/smaliTreeWalker.g b/smali/src/main/antlr/smaliTreeWalker.g index d074579b..171756ec 100644 --- a/smali/src/main/antlr/smaliTreeWalker.g +++ b/smali/src/main/antlr/smaliTreeWalker.g @@ -85,8 +85,8 @@ import java.util.*; this.dexBuilder = dexBuilder; } - public void setApiLevel(int apiLevel, boolean experimental) { - this.opcodes = new Opcodes(apiLevel, experimental); + public void setApiLevel(int apiLevel) { + this.opcodes = Opcodes.forApi(apiLevel); this.apiLevel = apiLevel; } diff --git a/smali/src/main/java/org/jf/smali/AssembleCommand.java b/smali/src/main/java/org/jf/smali/AssembleCommand.java new file mode 100644 index 00000000..efde1825 --- /dev/null +++ b/smali/src/main/java/org/jf/smali/AssembleCommand.java @@ -0,0 +1,113 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.smali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.beust.jcommander.validators.PositiveInteger; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedParameter; +import org.jf.util.jcommander.ExtendedParameters; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.util.List; + +@Parameters(commandDescription = "Assembles smali files into a dex file.") +@ExtendedParameters( + commandName = "assemble", + commandAliases = { "ass", "as", "a" }) +public class AssembleCommand extends Command { + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information for this command.") + private boolean help; + + @Parameter(names = {"-j", "--jobs"}, + description = "The number of threads to use. Defaults to the number of cores available.", + validateWith = PositiveInteger.class) + @ExtendedParameter(argumentNames = "n") + private int jobs = Runtime.getRuntime().availableProcessors(); + + @Parameter(names = {"-a", "--api"}, + description = "The numeric api level to use while assembling.") + @ExtendedParameter(argumentNames = "api") + private int apiLevel = 15; + + @Parameter(names = {"-o", "--output"}, + description = "The name/path of the dex file to write.") + @ExtendedParameter(argumentNames = "file") + private String output = "out.dex"; + + @Parameter(names = "--verbose", + description = "Generate verbose error messages.") + private boolean verbose = false; + + @Parameter(names = {"--allow-odex-opcodes", "--allow-odex", "--ao"}, + description = "Allows the odex opcodes that dalvik doesn't reject to be assembled.") + private boolean allowOdexOpcodes; + + @Parameter(description = "Assembles the given files. If a directory is specified, it will be " + + "recursively searched for any files with a .smali prefix") + @ExtendedParameter(argumentNames = "[<file>|<dir>]+") + private List<String> input; + + public AssembleCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + @Override public void run() { + if (help || input == null || input.isEmpty()) { + usage(); + return; + } + + try { + Smali.assemble(getOptions(), input); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + protected SmaliOptions getOptions() { + SmaliOptions options = new SmaliOptions(); + + options.jobs = jobs; + options.apiLevel = apiLevel; + options.outputDexFile = output; + options.allowOdexOpcodes = allowOdexOpcodes; + options.verboseErrors = verbose; + + return options; + } +} diff --git a/smali/src/main/java/org/jf/smali/HelpCommand.java b/smali/src/main/java/org/jf/smali/HelpCommand.java new file mode 100644 index 00000000..429a7dfd --- /dev/null +++ b/smali/src/main/java/org/jf/smali/HelpCommand.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.smali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import org.jf.util.ConsoleUtil; +import org.jf.util.jcommander.*; + +import javax.annotation.Nonnull; +import java.util.List; + +@Parameters(commandDescription = "Shows usage information") +@ExtendedParameters( + commandName = "help", + commandAliases = "h") +public class HelpCommand extends Command { + + @Parameter(description = "If specified, show the detailed usage information for the given commands") + @ExtendedParameter(argumentNames = "commands") + private List<String> commands; + + public HelpCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + + public void run() { + JCommander parentJc = commandAncestors.get(commandAncestors.size() - 1); + + if (commands == null || commands.isEmpty()) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); + } else { + boolean printedHelp = false; + for (String cmd : commands) { + JCommander command = ExtendedCommands.getSubcommand(parentJc, cmd); + if (command == null) { + System.err.println("No such command: " + cmd); + } else { + printedHelp = true; + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(((Command)command.getObjects().get(0)).getCommandHierarchy())); + } + } + if (!printedHelp) { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(commandAncestors)); + } + } + } + + @Parameters(hidden = true) + @ExtendedParameters(commandName = "hlep") + public static class HlepCommand extends HelpCommand { + public HlepCommand(@Nonnull List<JCommander> commandAncestors) { + super(commandAncestors); + } + } +} diff --git a/smali/src/main/java/org/jf/smali/Main.java b/smali/src/main/java/org/jf/smali/Main.java new file mode 100644 index 00000000..6b56fddb --- /dev/null +++ b/smali/src/main/java/org/jf/smali/Main.java @@ -0,0 +1,123 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.smali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.google.common.collect.Lists; +import org.jf.smali.HelpCommand.HlepCommand; +import org.jf.util.jcommander.Command; +import org.jf.util.jcommander.ExtendedCommands; +import org.jf.util.jcommander.ExtendedParameters; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Properties; + +@ExtendedParameters( + includeParametersInUsage = true, + commandName = "smali", + postfixDescription = "See smali help <command> for more information about a specific command") +public class Main extends Command { + public static final String VERSION = loadVersion(); + + @Parameter(names = {"-h", "-?", "--help"}, help = true, + description = "Show usage information") + private boolean help; + + @Parameter(names = {"-v", "--version"}, help = true, + description = "Print the version of baksmali and then exit") + public boolean version; + + private JCommander jc; + + @Override public void run() { + } + + @Override protected JCommander getJCommander() { + return jc; + } + + public Main() { + super(Lists.<JCommander>newArrayList()); + } + + public static void main(String[] args) { + Main main = new Main(); + + JCommander jc = new JCommander(main); + main.jc = jc; + jc.setProgramName("smali"); + List<JCommander> commandHierarchy = main.getCommandHierarchy(); + + ExtendedCommands.addExtendedCommand(jc, new AssembleCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new HelpCommand(commandHierarchy)); + ExtendedCommands.addExtendedCommand(jc, new HlepCommand(commandHierarchy)); + + jc.parse(args); + + if (main.version) { + version(); + } + + if (jc.getParsedCommand() == null || main.help) { + main.usage(); + return; + } + + Command command = (Command)jc.getCommands().get(jc.getParsedCommand()).getObjects().get(0); + command.run(); + } + + protected static void version() { + System.out.println("smali " + VERSION + " (http://smali.org)"); + System.out.println("Copyright (C) 2010 Ben Gruver (JesusFreke@JesusFreke.com)"); + System.out.println("BSD license (http://www.opensource.org/licenses/bsd-license.php)"); + System.exit(0); + } + + private static String loadVersion() { + InputStream propertiesStream = Main.class.getClassLoader().getResourceAsStream("smali.properties"); + String version = "[unknown version]"; + if (propertiesStream != null) { + Properties properties = new Properties(); + try { + properties.load(propertiesStream); + version = properties.getProperty("application.version"); + } catch (IOException ex) { + // ignore + } + } + return version; + } +} diff --git a/smali/src/main/java/org/jf/smali/Smali.java b/smali/src/main/java/org/jf/smali/Smali.java new file mode 100644 index 00000000..7f3762af --- /dev/null +++ b/smali/src/main/java/org/jf/smali/Smali.java @@ -0,0 +1,208 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.smali; + +import com.google.common.collect.Lists; +import org.antlr.runtime.CommonTokenStream; +import org.antlr.runtime.Token; +import org.antlr.runtime.TokenSource; +import org.antlr.runtime.tree.CommonTree; +import org.antlr.runtime.tree.CommonTreeNodeStream; +import org.jf.dexlib2.Opcodes; +import org.jf.dexlib2.writer.builder.DexBuilder; +import org.jf.dexlib2.writer.io.FileDataStore; + +import javax.annotation.Nonnull; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.*; + +public class Smali { + + /** + * Assemble the specified files, using the given options + * + * @param options a SmaliOptions object with the options to run smali with + * @param input The files/directories to process + * @return true if assembly completed with no errors, or false if errors were encountered + */ + public static boolean assemble(final SmaliOptions options, String... input) throws IOException { + return assemble(options, Arrays.asList(input)); + } + + /** + * Assemble the specified files, using the given options + * + * @param options a SmaliOptions object with the options to run smali with + * @param input The files/directories to process + * @return true if assembly completed with no errors, or false if errors were encountered + */ + public static boolean assemble(final SmaliOptions options, List<String> input) throws IOException { + LinkedHashSet<File> filesToProcessSet = new LinkedHashSet<File>(); + + for (String fileToProcess: input) { + File argFile = new File(fileToProcess); + + if (!argFile.exists()) { + throw new IllegalArgumentException("Cannot find file or directory \"" + fileToProcess + "\""); + } + + if (argFile.isDirectory()) { + getSmaliFilesInDir(argFile, filesToProcessSet); + } else if (argFile.isFile()) { + filesToProcessSet.add(argFile); + } + } + + boolean errors = false; + + final DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(options.apiLevel)); + + ExecutorService executor = Executors.newFixedThreadPool(options.jobs); + List<Future<Boolean>> tasks = Lists.newArrayList(); + + for (final File file: filesToProcessSet) { + tasks.add(executor.submit(new Callable<Boolean>() { + @Override public Boolean call() throws Exception { + return assembleSmaliFile(file, dexBuilder, options); + } + })); + } + + for (Future<Boolean> task: tasks) { + while(true) { + try { + try { + if (!task.get()) { + errors = true; + } + } catch (ExecutionException ex) { + throw new RuntimeException(ex); + } + } catch (InterruptedException ex) { + continue; + } + break; + } + } + + executor.shutdown(); + + if (errors) { + return false; + } + + dexBuilder.writeTo(new FileDataStore(new File(options.outputDexFile))); + + return true; + } + + private static void getSmaliFilesInDir(@Nonnull File dir, @Nonnull Set<File> smaliFiles) { + File[] files = dir.listFiles(); + if (files != null) { + for(File file: files) { + if (file.isDirectory()) { + getSmaliFilesInDir(file, smaliFiles); + } else if (file.getName().endsWith(".smali")) { + smaliFiles.add(file); + } + } + } + } + + private static boolean assembleSmaliFile(File smaliFile, DexBuilder dexBuilder, SmaliOptions options) + throws Exception { + FileInputStream fis = null; + try { + fis = new FileInputStream(smaliFile); + InputStreamReader reader = new InputStreamReader(fis, "UTF-8"); + + LexerErrorInterface lexer = new smaliFlexLexer(reader); + ((smaliFlexLexer)lexer).setSourceFile(smaliFile); + CommonTokenStream tokens = new CommonTokenStream((TokenSource)lexer); + + if (options.printTokens) { + tokens.getTokens(); + + for (int i=0; i<tokens.size(); i++) { + Token token = tokens.get(i); + if (token.getChannel() == smaliParser.HIDDEN) { + continue; + } + + System.out.println(smaliParser.tokenNames[token.getType()] + ": " + token.getText()); + } + + System.out.flush(); + } + + smaliParser parser = new smaliParser(tokens); + parser.setVerboseErrors(options.verboseErrors); + parser.setAllowOdex(options.allowOdexOpcodes); + parser.setApiLevel(options.apiLevel); + + smaliParser.smali_file_return result = parser.smali_file(); + + if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) { + return false; + } + + CommonTree t = result.getTree(); + + CommonTreeNodeStream treeStream = new CommonTreeNodeStream(t); + treeStream.setTokenStream(tokens); + + if (options.printTokens) { + System.out.println(t.toStringTree()); + } + + smaliTreeWalker dexGen = new smaliTreeWalker(treeStream); + dexGen.setApiLevel(options.apiLevel); + + dexGen.setVerboseErrors(options.verboseErrors); + dexGen.setDexBuilder(dexBuilder); + dexGen.smali_file(); + + return dexGen.getNumberOfSyntaxErrors() == 0; + } finally { + if (fis != null) { + fis.close(); + } + } + } +} diff --git a/smali/src/main/java/org/jf/smali/SmaliOptions.java b/smali/src/main/java/org/jf/smali/SmaliOptions.java index 165c3a89..ac385fe6 100644 --- a/smali/src/main/java/org/jf/smali/SmaliOptions.java +++ b/smali/src/main/java/org/jf/smali/SmaliOptions.java @@ -36,17 +36,7 @@ public class SmaliOptions { public String outputDexFile = "out.dex"; public int jobs = Runtime.getRuntime().availableProcessors(); - public boolean allowOdex = false; + public boolean allowOdexOpcodes = false; public boolean verboseErrors = false; public boolean printTokens = false; - public boolean experimental = false; - - public boolean listMethods = false; - public String methodListFilename = null; - - public boolean listFields = false; - public String fieldListFilename = null; - - public boolean listTypes = false; - public String typeListFilename = null; } diff --git a/smali/src/main/java/org/jf/smali/SmaliTestUtils.java b/smali/src/main/java/org/jf/smali/SmaliTestUtils.java index bef07414..a0fe55c2 100644 --- a/smali/src/main/java/org/jf/smali/SmaliTestUtils.java +++ b/smali/src/main/java/org/jf/smali/SmaliTestUtils.java @@ -50,14 +50,14 @@ import java.io.StringReader; public class SmaliTestUtils { public static ClassDef compileSmali(String smaliText) throws RecognitionException, IOException { - return compileSmali(smaliText, 15, false); + return compileSmali(smaliText, 15); } - public static ClassDef compileSmali(String smaliText, int apiLevel, boolean experimental) + public static ClassDef compileSmali(String smaliText, int apiLevel) throws RecognitionException, IOException { CommonTokenStream tokens; LexerErrorInterface lexer; - DexBuilder dexBuilder = DexBuilder.makeDexBuilder(Opcodes.forApi(apiLevel, experimental)); + DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(apiLevel)); Reader reader = new StringReader(smaliText); @@ -67,7 +67,7 @@ public class SmaliTestUtils { smaliParser parser = new smaliParser(tokens); parser.setVerboseErrors(true); parser.setAllowOdex(false); - parser.setApiLevel(apiLevel, experimental); + parser.setApiLevel(apiLevel); smaliParser.smali_file_return result = parser.smali_file(); @@ -81,7 +81,7 @@ public class SmaliTestUtils { treeStream.setTokenStream(tokens); smaliTreeWalker dexGen = new smaliTreeWalker(treeStream); - dexGen.setApiLevel(apiLevel, experimental); + dexGen.setApiLevel(apiLevel); dexGen.setVerboseErrors(true); dexGen.setDexBuilder(dexBuilder); dexGen.smali_file(); @@ -94,7 +94,7 @@ public class SmaliTestUtils { dexBuilder.writeTo(dataStore); - DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(apiLevel, experimental), dataStore.getData()); + DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(apiLevel), dataStore.getData()); return Iterables.getFirst(dexFile.getClasses(), null); } diff --git a/smali/src/main/java/org/jf/smali/main.java b/smali/src/main/java/org/jf/smali/main.java deleted file mode 100644 index e5562808..00000000 --- a/smali/src/main/java/org/jf/smali/main.java +++ /dev/null @@ -1,491 +0,0 @@ -/* - * [The "BSD licence"] - * Copyright (c) 2010 Ben Gruver (JesusFreke) - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.jf.smali; - -import com.google.common.base.Strings; -import com.google.common.collect.Lists; -import com.google.common.collect.Ordering; -import org.antlr.runtime.CommonTokenStream; -import org.antlr.runtime.Token; -import org.antlr.runtime.TokenSource; -import org.antlr.runtime.tree.CommonTree; -import org.antlr.runtime.tree.CommonTreeNodeStream; -import org.apache.commons.cli.*; -import org.jf.dexlib2.Opcodes; -import org.jf.dexlib2.writer.builder.DexBuilder; -import org.jf.dexlib2.writer.io.FileDataStore; -import org.jf.util.ConsoleUtil; -import org.jf.util.SmaliHelpFormatter; - -import javax.annotation.Nonnull; -import java.io.*; -import java.util.*; -import java.util.concurrent.*; - -/** - * Main class for smali. It recognizes enough options to be able to dispatch - * to the right "actual" main. - */ -public class main { - - public static final String VERSION; - - private final static Options basicOptions; - private final static Options debugOptions; - private final static Options options; - - static { - basicOptions = new Options(); - debugOptions = new Options(); - options = new Options(); - buildOptions(); - - InputStream templateStream = main.class.getClassLoader().getResourceAsStream("smali.properties"); - if (templateStream != null) { - Properties properties = new Properties(); - String version = "(unknown)"; - try { - properties.load(templateStream); - version = properties.getProperty("application.version"); - } catch (IOException ex) { - // just eat it - } - VERSION = version; - } else { - VERSION = "[unknown version]"; - } - } - - - /** - * This class is uninstantiable. - */ - private main() { - } - - /** - * A more programmatic-friendly entry point for smali - * - * @param options a SmaliOptions object with the options to run smali with - * @param input The files/directories to process - * @return true if assembly completed with no errors, or false if errors were encountered - */ - public static boolean run(final SmaliOptions options, String... input) throws IOException { - LinkedHashSet<File> filesToProcessSet = new LinkedHashSet<File>(); - - for (String fileToProcess: input) { - File argFile = new File(fileToProcess); - - if (!argFile.exists()) { - throw new IllegalArgumentException("Cannot find file or directory \"" + fileToProcess + "\""); - } - - if (argFile.isDirectory()) { - getSmaliFilesInDir(argFile, filesToProcessSet); - } else if (argFile.isFile()) { - filesToProcessSet.add(argFile); - } - } - - boolean errors = false; - - final DexBuilder dexBuilder = DexBuilder.makeDexBuilder( - Opcodes.forApi(options.apiLevel, options.experimental)); - - ExecutorService executor = Executors.newFixedThreadPool(options.jobs); - List<Future<Boolean>> tasks = Lists.newArrayList(); - - for (final File file: filesToProcessSet) { - tasks.add(executor.submit(new Callable<Boolean>() { - @Override public Boolean call() throws Exception { - return assembleSmaliFile(file, dexBuilder, options); - } - })); - } - - for (Future<Boolean> task: tasks) { - while(true) { - try { - try { - if (!task.get()) { - errors = true; - } - } catch (ExecutionException ex) { - throw new RuntimeException(ex); - } - } catch (InterruptedException ex) { - continue; - } - break; - } - } - - executor.shutdown(); - - if (errors) { - return false; - } - - if (options.listMethods) { - if (Strings.isNullOrEmpty(options.methodListFilename)) { - options.methodListFilename = options.outputDexFile + ".methods"; - } - writeReferences(dexBuilder.getMethodReferences(), options.methodListFilename); - } - - if (options.listFields) { - if (Strings.isNullOrEmpty(options.fieldListFilename)) { - options.fieldListFilename = options.outputDexFile + ".fields"; - } - writeReferences(dexBuilder.getFieldReferences(), options.fieldListFilename); - } - - if (options.listTypes) { - if (Strings.isNullOrEmpty(options.typeListFilename)) { - options.typeListFilename = options.outputDexFile + ".types"; - } - writeReferences(dexBuilder.getTypeReferences(), options.typeListFilename); - } - - dexBuilder.writeTo(new FileDataStore(new File(options.outputDexFile))); - - return true; - } - - /** - * Run! - */ - public static void main(String[] args) { - Locale locale = new Locale("en", "US"); - Locale.setDefault(locale); - - CommandLineParser parser = new PosixParser(); - CommandLine commandLine; - - try { - commandLine = parser.parse(options, args); - } catch (ParseException ex) { - usage(); - return; - } - - SmaliOptions smaliOptions = new SmaliOptions(); - - String[] remainingArgs = commandLine.getArgs(); - - Option[] options = commandLine.getOptions(); - - for (int i=0; i<options.length; i++) { - Option option = options[i]; - String opt = option.getOpt(); - - switch (opt.charAt(0)) { - case 'v': - version(); - return; - case '?': - while (++i < options.length) { - if (options[i].getOpt().charAt(0) == '?') { - usage(true); - return; - } - } - usage(false); - return; - case 'o': - smaliOptions.outputDexFile = commandLine.getOptionValue("o"); - break; - case 'x': - smaliOptions.allowOdex = true; - break; - case 'X': - smaliOptions.experimental = true; - break; - case 'a': - smaliOptions.apiLevel = Integer.parseInt(commandLine.getOptionValue("a")); - break; - case 'j': - smaliOptions.jobs = Integer.parseInt(commandLine.getOptionValue("j")); - break; - case 'm': - smaliOptions.listMethods = true; - smaliOptions.methodListFilename = commandLine.getOptionValue("m"); - break; - case 'f': - smaliOptions.listFields = true; - smaliOptions.fieldListFilename = commandLine.getOptionValue("f"); - break; - case 't': - smaliOptions.listTypes = true; - smaliOptions.typeListFilename = commandLine.getOptionValue("t"); - break; - case 'V': - smaliOptions.verboseErrors = true; - break; - case 'T': - smaliOptions.printTokens = true; - break; - default: - assert false; - } - } - - if (remainingArgs.length == 0) { - usage(); - return; - } - - try { - if (!run(smaliOptions, remainingArgs)) { - System.exit(1); - } - } catch (RuntimeException ex) { - System.err.println("\nUNEXPECTED TOP-LEVEL EXCEPTION:"); - ex.printStackTrace(); - System.exit(2); - } catch (Throwable ex) { - System.err.println("\nUNEXPECTED TOP-LEVEL ERROR:"); - ex.printStackTrace(); - System.exit(3); - } - } - - private static void writeReferences(List<String> references, String filename) { - PrintWriter writer = null; - try { - writer = new PrintWriter(new BufferedWriter(new FileWriter(filename))); - - for (String reference: Ordering.natural().sortedCopy(references)) { - writer.println(reference); - } - } catch (IOException ex) { - throw new RuntimeException(ex); - } finally { - if (writer != null) { - writer.close(); - } - } - } - - private static void getSmaliFilesInDir(@Nonnull File dir, @Nonnull Set<File> smaliFiles) { - File[] files = dir.listFiles(); - if (files != null) { - for(File file: files) { - if (file.isDirectory()) { - getSmaliFilesInDir(file, smaliFiles); - } else if (file.getName().endsWith(".smali")) { - smaliFiles.add(file); - } - } - } - } - - private static boolean assembleSmaliFile(File smaliFile, DexBuilder dexBuilder, SmaliOptions options) - throws Exception { - CommonTokenStream tokens; - - LexerErrorInterface lexer; - - FileInputStream fis = new FileInputStream(smaliFile); - InputStreamReader reader = new InputStreamReader(fis, "UTF-8"); - - lexer = new smaliFlexLexer(reader); - ((smaliFlexLexer)lexer).setSourceFile(smaliFile); - tokens = new CommonTokenStream((TokenSource)lexer); - - if (options.printTokens) { - tokens.getTokens(); - - for (int i=0; i<tokens.size(); i++) { - Token token = tokens.get(i); - if (token.getChannel() == smaliParser.HIDDEN) { - continue; - } - - System.out.println(smaliParser.tokenNames[token.getType()] + ": " + token.getText()); - } - - System.out.flush(); - } - - smaliParser parser = new smaliParser(tokens); - parser.setVerboseErrors(options.verboseErrors); - parser.setAllowOdex(options.allowOdex); - parser.setApiLevel(options.apiLevel, options.experimental); - - smaliParser.smali_file_return result = parser.smali_file(); - - if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) { - return false; - } - - CommonTree t = result.getTree(); - - CommonTreeNodeStream treeStream = new CommonTreeNodeStream(t); - treeStream.setTokenStream(tokens); - - if (options.printTokens) { - System.out.println(t.toStringTree()); - } - - smaliTreeWalker dexGen = new smaliTreeWalker(treeStream); - dexGen.setApiLevel(options.apiLevel, options.experimental); - - dexGen.setVerboseErrors(options.verboseErrors); - dexGen.setDexBuilder(dexBuilder); - dexGen.smali_file(); - - return dexGen.getNumberOfSyntaxErrors() == 0; - } - - - /** - * Prints the usage message. - */ - private static void usage(boolean printDebugOptions) { - SmaliHelpFormatter formatter = new SmaliHelpFormatter(); - - int consoleWidth = ConsoleUtil.getConsoleWidth(); - if (consoleWidth <= 0) { - consoleWidth = 80; - } - - formatter.setWidth(consoleWidth); - - formatter.printHelp("java -jar smali.jar [options] [--] [<smali-file>|folder]*", - "assembles a set of smali files into a dex file", basicOptions, printDebugOptions?debugOptions:null); - } - - private static void usage() { - usage(false); - } - - /** - * Prints the version message. - */ - private static void version() { - System.out.println("smali " + VERSION + " (http://smali.googlecode.com)"); - System.out.println("Copyright (C) 2010 Ben Gruver (JesusFreke@JesusFreke.com)"); - System.out.println("BSD license (http://www.opensource.org/licenses/bsd-license.php)"); - System.exit(0); - } - - @SuppressWarnings("AccessStaticViaInstance") - private static void buildOptions() { - Option versionOption = OptionBuilder.withLongOpt("version") - .withDescription("prints the version then exits") - .create("v"); - - Option helpOption = OptionBuilder.withLongOpt("help") - .withDescription("prints the help message then exits. Specify twice for debug options") - .create("?"); - - Option outputOption = OptionBuilder.withLongOpt("output") - .withDescription("the name of the dex file that will be written. The default is out.dex") - .hasArg() - .withArgName("FILE") - .create("o"); - - Option allowOdexOption = OptionBuilder.withLongOpt("allow-odex-instructions") - .withDescription("allow odex instructions to be compiled into the dex file. Only a few" + - " instructions are supported - the ones that can exist in a dead code path and not" + - " cause dalvik to reject the class") - .create("x"); - - Option apiLevelOption = OptionBuilder.withLongOpt("api-level") - .withDescription("The numeric api-level of the file to generate, e.g. 14 for ICS. If not " + - "specified, it defaults to 15 (ICS).") - .hasArg() - .withArgName("API_LEVEL") - .create("a"); - - Option listMethodsOption = OptionBuilder.withLongOpt("list-methods") - .withDescription("Lists all the method references to FILE" + - " (<output_dex_filename>.methods by default)") - .hasOptionalArg() - .withArgName("FILE") - .create("m"); - - Option listFieldsOption = OptionBuilder.withLongOpt("list-fields") - .withDescription("Lists all the field references to FILE" + - " (<output_dex_filename>.fields by default)") - .hasOptionalArg() - .withArgName("FILE") - .create("f"); - - Option listClassesOption = OptionBuilder.withLongOpt("list-types") - .withDescription("Lists all the type references to FILE" + - " (<output_dex_filename>.types by default)") - .hasOptionalArg() - .withArgName("FILE") - .create("t"); - - Option experimentalOption = OptionBuilder.withLongOpt("experimental") - .withDescription("enable experimental opcodes to be assembled, even if they " + - " aren't necessarily supported by the Android runtime yet") - .create("X"); - - Option jobsOption = OptionBuilder.withLongOpt("jobs") - .withDescription("The number of threads to use. Defaults to the number of cores available, up to a " + - "maximum of 6") - .hasArg() - .withArgName("NUM_THREADS") - .create("j"); - - Option verboseErrorsOption = OptionBuilder.withLongOpt("verbose-errors") - .withDescription("Generate verbose error messages") - .create("V"); - - Option printTokensOption = OptionBuilder.withLongOpt("print-tokens") - .withDescription("Print the name and text of each token") - .create("T"); - - basicOptions.addOption(versionOption); - basicOptions.addOption(helpOption); - basicOptions.addOption(outputOption); - basicOptions.addOption(allowOdexOption); - basicOptions.addOption(apiLevelOption); - basicOptions.addOption(experimentalOption); - basicOptions.addOption(jobsOption); - basicOptions.addOption(listMethodsOption); - basicOptions.addOption(listFieldsOption); - basicOptions.addOption(listClassesOption); - - debugOptions.addOption(verboseErrorsOption); - debugOptions.addOption(printTokensOption); - - for (Object option: basicOptions.getOptions()) { - options.addOption((Option)option); - } - - for (Object option: debugOptions.getOptions()) { - options.addOption((Option)option); - } - } -}
\ No newline at end of file diff --git a/smalidea/build.gradle b/smalidea/build.gradle index 8cba19d2..57209762 100644 --- a/smalidea/build.gradle +++ b/smalidea/build.gradle @@ -34,9 +34,13 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } + maven { + url 'http://dl.bintray.com/jetbrains/intellij-plugin-service' + } + } dependencies { - classpath 'gradle.plugin.org.jetbrains:gradle-intellij-plugin:0.0.40' + classpath "gradle.plugin.org.jetbrains:gradle-intellij-plugin:0.1.10" } } @@ -44,7 +48,8 @@ apply plugin: 'java' apply plugin: 'idea' apply plugin: 'antlr' -version = '0.03' + +version = '0.05' if (!('release' in gradle.startParameter.taskNames)) { def versionSuffix @@ -86,14 +91,12 @@ if (System.env.JDK7_HOME != null) { def sandboxDir = "${buildDir}/sandbox" -// We don't want to use the org.jetbrains.intellij plugin when generating the idea project files, -// so that idea classes aren't included as project dependencies, since they will already exist -// in the plugin sdk defined for the project if (!('idea' in gradle.startParameter.taskNames)) { + apply plugin: 'org.jetbrains.intellij' intellij { - version 'IC-15.0.6' + version 'IC-2016.3.5' pluginName 'smalidea' updateSinceUntilBuild false @@ -106,8 +109,8 @@ if (!('idea' in gradle.startParameter.taskNames)) { task ideaDirs() { project.afterEvaluate { if (intellij != null) { - println "IDEA Plugin jdk: ${intellij.ideaDirectory}" - println "sources: ${project.configurations['intellij-sources'].files[0]}" + println "IDEA Plugin jdk: ${intellij.ideaDependency.classes}" + println "sources: ${intellij.ideaDependency.getSources()}" } } } @@ -115,13 +118,8 @@ if (!('idea' in gradle.startParameter.taskNames)) { dependencies { compile files("${System.properties['java.home']}/../lib/tools.jar") } -} else { - // If we're running the idea task, let's make sure nothing else is being run, since - // we have to use a special configuration for the idea task - if (gradle.startParameter.taskNames.size() > 1) { - throw new InvalidUserDataException("The idea task must be run by itself.") - } +} else { project(':') { idea { project { @@ -209,13 +207,15 @@ dependencies { } task extractTokens(type: org.gradle.api.tasks.Copy, dependsOn: ':smali:build') { - def allArtifacts = configurations.default.resolvedConfiguration.resolvedArtifacts - def smaliArtifact = allArtifacts.find { it.moduleVersion.id.name.equals('smali') } + project.afterEvaluate { + def allArtifacts = configurations.default.resolvedConfiguration.resolvedArtifacts + def smaliArtifact = allArtifacts.find { it.moduleVersion.id.name.equals('smali') } - from(zipTree(smaliArtifact.file)) { - include '**/*.tokens' + from(zipTree(smaliArtifact.file)) { + include '**/*.tokens' + } + into "${buildDir}/tokens" } - into "${buildDir}/tokens" } generateGrammarSource { @@ -225,7 +225,6 @@ generateGrammarSource { outputDirectory(file("${buildDir}/generated-src/antlr/main/org/jf/smalidea")) } generateGrammarSource.dependsOn(extractTokens) - ideaModule.dependsOn(generateGrammarSource) task release(dependsOn: 'buildPlugin') { diff --git a/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliCodeFragmentFactory.java b/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliCodeFragmentFactory.java index 5e2dd0c9..82a190b7 100644 --- a/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliCodeFragmentFactory.java +++ b/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliCodeFragmentFactory.java @@ -61,6 +61,7 @@ import org.jf.smalidea.psi.impl.SmaliMethod; import org.jf.smalidea.util.NameUtils; import org.jf.smalidea.util.PsiUtil; +import javax.annotation.Nullable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.List; @@ -274,9 +275,14 @@ public class SmaliCodeFragmentFactory extends DefaultCodeFragmentFactory { return originalContext; } + @Nullable public static Value evaluateRegister(EvaluationContext context, final SmaliMethod smaliMethod, final int registerNum, final String type) throws EvaluateException { + if (registerNum >= smaliMethod.getRegisterCount()) { + return null; + } + final StackFrameProxy frameProxy = context.getSuspendContext().getFrameProxy(); if (frameProxy == null) { return null; @@ -304,12 +310,21 @@ public class SmaliCodeFragmentFactory extends DefaultCodeFragmentFactory { for (SmaliInstruction instruction: smaliMethod.getInstructions()) { methodSize += instruction.getInstructionSize(); } - Location endLocation = method.locationOfCodeIndex((methodSize/2) - 1); + Location endLocation = null; + for (int endCodeIndex = (methodSize/2) - 1; endCodeIndex >= 0; endCodeIndex--) { + endLocation = method.locationOfCodeIndex(endCodeIndex); + if (endLocation != null) { + break; + } + } + if (endLocation == null) { + return null; + } LocalVariable localVariable = localVariableConstructor.newInstance(vm, method, mapRegister(frameProxy.getStackFrame().virtualMachine(), smaliMethod, registerNum), - method.locationOfCodeIndex(0), + method.location(), endLocation, String.format("v%d", registerNum), type, null); diff --git a/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliPositionManager.java b/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliPositionManager.java index 781a8569..9c0abbe6 100644 --- a/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliPositionManager.java +++ b/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliPositionManager.java @@ -38,6 +38,7 @@ import com.intellij.debugger.engine.DebugProcess; import com.intellij.debugger.requests.ClassPrepareRequestor; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.util.Computable; +import com.intellij.psi.PsiFile; import com.intellij.psi.search.GlobalSearchScope; import com.sun.jdi.Location; import com.sun.jdi.ReferenceType; @@ -60,10 +61,16 @@ public class SmaliPositionManager implements PositionManager { this.debugProcess = debugProcess; } - public SourcePosition getSourcePosition(String declaringType, String methodName, String methodSignature, + public SourcePosition getSourcePosition(final String declaringType, String methodName, String methodSignature, int codeIndex) throws NoDataException { - Collection<SmaliClass> classes = SmaliClassNameIndex.INSTANCE.get(declaringType, - debugProcess.getProject(), GlobalSearchScope.projectScope(debugProcess.getProject())); + + Collection<SmaliClass> classes = ApplicationManager.getApplication().runReadAction( + new Computable<Collection<SmaliClass>>() { + @Override public Collection<SmaliClass> compute() { + return SmaliClassNameIndex.INSTANCE.get(declaringType, debugProcess.getProject(), + GlobalSearchScope.projectScope(debugProcess.getProject())); + } + }); if (classes.size() > 0) { SmaliClass smaliClass = classes.iterator().next(); @@ -116,7 +123,13 @@ public class SmaliPositionManager implements PositionManager { @Override @NotNull public List<Location> locationsOfLine(@NotNull final ReferenceType type, @NotNull final SourcePosition position) throws NoDataException { - if (!(position.getElementAt().getContainingFile() instanceof SmaliFile)) { + PsiFile containingFile = ApplicationManager.getApplication().runReadAction(new Computable<PsiFile>() { + @Override public PsiFile compute() { + return position.getElementAt().getContainingFile(); + } + }); + + if (!(containingFile instanceof SmaliFile)) { throw NoDataException.INSTANCE; } @@ -125,6 +138,8 @@ public class SmaliPositionManager implements PositionManager { ApplicationManager.getApplication().runReadAction(new Runnable() { @Override public void run() { + + String typeName = type.name(); Collection<SmaliClass> classes = SmaliClassNameIndex.INSTANCE.get(typeName, debugProcess.getProject(), GlobalSearchScope.projectScope(debugProcess.getProject())); diff --git a/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyStringReference.java b/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyStringReference.java index 88fd0070..2b52e2b7 100644 --- a/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyStringReference.java +++ b/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyStringReference.java @@ -32,6 +32,8 @@ package org.jf.smalidea.debugging.value; import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiSubstitutor; +import com.sun.jdi.ObjectReference; import com.sun.jdi.StringReference; import org.jf.smalidea.psi.impl.SmaliMethod; @@ -41,6 +43,11 @@ public class LazyStringReference extends LazyObjectReference<StringReference> im } public String value() { + ObjectReference objectReference = getValue(); + if (!(objectReference instanceof StringReference)) { + throw new IllegalStateException(String.format("Expecting type String, but got %s. method=%s, register=%d", + objectReference.type().name(), this.method.getSignature(PsiSubstitutor.EMPTY), registerNumber)); + } return getValue().value(); } } diff --git a/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyValue.java b/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyValue.java index 0eeb010b..f17df6d7 100644 --- a/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyValue.java +++ b/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyValue.java @@ -48,10 +48,10 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; public class LazyValue<T extends Value> implements Value { - private final int registerNumber; - private final Project project; - private final SmaliMethod method; - private final String type; + protected final int registerNumber; + protected final Project project; + protected final SmaliMethod method; + protected final String type; private EvaluationContext evaluationContext; private Value value; diff --git a/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaPackedSwitchPayload.java b/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaPackedSwitchPayload.java index 9d2a0fc3..6056da37 100644 --- a/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaPackedSwitchPayload.java +++ b/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaPackedSwitchPayload.java @@ -86,7 +86,7 @@ public class SmalideaPackedSwitchPayload extends SmalideaInstruction implements return 0; } - return label.getOffset() - baseOffset; + return (label.getOffset() - baseOffset) / 2; } }); } diff --git a/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaSparseSwitchPayload.java b/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaSparseSwitchPayload.java index 15eaea2d..832c8807 100644 --- a/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaSparseSwitchPayload.java +++ b/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaSparseSwitchPayload.java @@ -79,7 +79,7 @@ public class SmalideaSparseSwitchPayload extends SmalideaInstruction implements return 0; } - return label.getOffset() - baseOffset; + return (label.getOffset() - baseOffset) / 2; } }; } diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliAnnotation.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliAnnotation.java index e36313ba..ac3dd81f 100644 --- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliAnnotation.java +++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliAnnotation.java @@ -106,7 +106,7 @@ public class SmaliAnnotation extends SmaliStubBasedPsiElement<SmaliAnnotationStu } @Nullable @Override public PsiAnnotationOwner getOwner() { - return (PsiAnnotationOwner)getStubOrPsiParent(); + return (PsiAnnotationOwner)getParentByStub(); } @Nullable @Override public PsiMetaData getMetaData() { diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliField.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliField.java index 7bef4e94..579401cf 100644 --- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliField.java +++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliField.java @@ -88,7 +88,7 @@ public class SmaliField extends SmaliStubBasedPsiElement<SmaliFieldStub> impleme } @Nullable @Override public PsiClass getContainingClass() { - return (PsiClass)getStubOrPsiParent(); + return (PsiClass)getParentByStub(); } @NotNull @Override public PsiType getType() { diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliImplementsList.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliImplementsList.java index 8992ab0b..fb4a788f 100644 --- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliImplementsList.java +++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliImplementsList.java @@ -56,7 +56,7 @@ public class SmaliImplementsList extends SmaliBaseReferenceList<SmaliImplementsL } @NotNull private SmaliClassTypeElement[] getImplementsElements() { - SmaliClass smaliClass = (SmaliClass)getStubOrPsiParent(); + SmaliClass smaliClass = (SmaliClass)getParentByStub(); assert smaliClass != null; SmaliImplementsStatement[] implementsStatements = smaliClass.getImplementsStatements(); diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliInstruction.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliInstruction.java index 8cb2d77a..ecbdbb33 100644 --- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliInstruction.java +++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliInstruction.java @@ -76,7 +76,7 @@ public class SmaliInstruction extends SmaliCompositeElement { assert instructionNode != null; // TODO: put a project level Opcodes instance with the appropriate api level somewhere - opcode = new Opcodes(15, false).getOpcodeByName(instructionNode.getText()); + opcode = Opcodes.getDefault().getOpcodeByName(instructionNode.getText()); if (opcode == null) { if (instructionNode.getText().equals(".packed-switch")) { return Opcode.PACKED_SWITCH_PAYLOAD; diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliMethod.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliMethod.java index 085585bb..8ba618b3 100644 --- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliMethod.java +++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliMethod.java @@ -259,7 +259,7 @@ public class SmaliMethod extends SmaliStubBasedPsiElement<SmaliMethodStub> } @Nullable @Override public SmaliClass getContainingClass() { - PsiElement parent = getStubOrPsiParent(); + PsiElement parent = getParentByStub(); if (parent instanceof SmaliClass) { return (SmaliClass) parent; } diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/index/SmaliClassFinder.java b/smalidea/src/main/java/org/jf/smalidea/psi/index/SmaliClassFinder.java index 0ebb1eec..87b2afff 100644 --- a/smalidea/src/main/java/org/jf/smalidea/psi/index/SmaliClassFinder.java +++ b/smalidea/src/main/java/org/jf/smalidea/psi/index/SmaliClassFinder.java @@ -31,6 +31,7 @@ package org.jf.smalidea.psi.index; +import com.intellij.openapi.project.Project; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElementFinder; import com.intellij.psi.search.GlobalSearchScope; @@ -40,9 +41,16 @@ import org.jf.smalidea.psi.impl.SmaliClass; import java.util.Collection; public class SmaliClassFinder extends PsiElementFinder { + + private final Project project; + + public SmaliClassFinder(Project project) { + this.project = project; + } + @Override public PsiClass findClass(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) { - Collection<SmaliClass> classes = SmaliClassNameIndex.INSTANCE.get(qualifiedName, scope.getProject(), scope); + Collection<SmaliClass> classes = SmaliClassNameIndex.INSTANCE.get(qualifiedName, project, scope); if (classes != null && classes.size() == 1) { return classes.iterator().next(); } diff --git a/smalidea/src/main/resources/META-INF/plugin.xml b/smalidea/src/main/resources/META-INF/plugin.xml index 8ae92dad..91326cfe 100644 --- a/smalidea/src/main/resources/META-INF/plugin.xml +++ b/smalidea/src/main/resources/META-INF/plugin.xml @@ -1,7 +1,7 @@ <idea-plugin version="2"> <id>org.jf.smalidea</id> <name>Smalidea</name> - <version>0.02</version> + <version>0.04</version> <vendor email="jesusfreke@jesusfreke.com" url="http://smali.org">JesusFreke</vendor> <description><![CDATA[ diff --git a/util/build.gradle b/util/build.gradle index 407ef71f..23d6a3af 100644 --- a/util/build.gradle +++ b/util/build.gradle @@ -30,9 +30,9 @@ */ dependencies { - compile depends.commons_cli compile depends.findbugs compile depends.guava + compile depends.jcommander testCompile depends.junit } diff --git a/util/src/main/java/org/jf/util/OldWrappedIndentingWriter.java b/util/src/main/java/org/jf/util/OldWrappedIndentingWriter.java new file mode 100644 index 00000000..f4577179 --- /dev/null +++ b/util/src/main/java/org/jf/util/OldWrappedIndentingWriter.java @@ -0,0 +1,184 @@ +/* + * Copyright 2013, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util; + +import java.io.FilterWriter; +import java.io.IOException; +import java.io.Writer; + +/** + * Writer that wraps another writer and passes width-limited and + * optionally-prefixed output to its subordinate. When lines are + * wrapped they are automatically indented based on the start of the + * line. + */ +public final class OldWrappedIndentingWriter extends FilterWriter { + /** null-ok; optional prefix for every line */ + private final String prefix; + + /** > 0; the maximum output width */ + private final int width; + + /** > 0; the maximum indent */ + private final int maxIndent; + + /** >= 0; current output column (zero-based) */ + private int column; + + /** whether indent spaces are currently being collected */ + private boolean collectingIndent; + + /** >= 0; current indent amount */ + private int indent; + + /** + * Constructs an instance. + * + * @param out non-null; writer to send final output to + * @param width >= 0; the maximum output width (not including + * <code>prefix</code>), or <code>0</code> for no maximum + * @param prefix non-null; the prefix for each line + */ + public OldWrappedIndentingWriter(Writer out, int width, String prefix) { + super(out); + + if (out == null) { + throw new NullPointerException("out == null"); + } + + if (width < 0) { + throw new IllegalArgumentException("width < 0"); + } + + if (prefix == null) { + throw new NullPointerException("prefix == null"); + } + + this.width = (width != 0) ? width : Integer.MAX_VALUE; + this.maxIndent = width >> 1; + this.prefix = (prefix.length() == 0) ? null : prefix; + + bol(); + } + + /** + * Constructs a no-prefix instance. + * + * @param out non-null; writer to send final output to + * @param width >= 0; the maximum output width (not including + * <code>prefix</code>), or <code>0</code> for no maximum + */ + public OldWrappedIndentingWriter(Writer out, int width) { + this(out, width, ""); + } + + /** {@inheritDoc} */ + @Override + public void write(int c) throws IOException { + synchronized (lock) { + if (collectingIndent) { + if (c == ' ') { + indent++; + if (indent >= maxIndent) { + indent = maxIndent; + collectingIndent = false; + } + } else { + collectingIndent = false; + } + } + + if ((column == width) && (c != '\n')) { + out.write('\n'); + column = 0; + /* + * Note: No else, so this should fall through to the next + * if statement. + */ + } + + if (column == 0) { + if (prefix != null) { + out.write(prefix); + } + + if (!collectingIndent) { + for (int i = 0; i < indent; i++) { + out.write(' '); + } + column = indent; + } + } + + out.write(c); + + if (c == '\n') { + bol(); + } else { + column++; + } + } + } + + /** {@inheritDoc} */ + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + synchronized (lock) { + while (len > 0) { + write(cbuf[off]); + off++; + len--; + } + } + } + + /** {@inheritDoc} */ + @Override + public void write(String str, int off, int len) throws IOException { + synchronized (lock) { + while (len > 0) { + write(str.charAt(off)); + off++; + len--; + } + } + } + + /** + * Indicates that output is at the beginning of a line. + */ + private void bol() { + column = 0; + collectingIndent = (maxIndent != 0); + indent = 0; + } +} diff --git a/util/src/main/java/org/jf/util/PathUtil.java b/util/src/main/java/org/jf/util/PathUtil.java index 91eb7584..9ba9f301 100644 --- a/util/src/main/java/org/jf/util/PathUtil.java +++ b/util/src/main/java/org/jf/util/PathUtil.java @@ -28,9 +28,12 @@ package org.jf.util; +import com.google.common.collect.Lists; + import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.List; public class PathUtil { private PathUtil() { @@ -44,19 +47,9 @@ public class PathUtil { return new File(getRelativeFileInternal(baseFile.getCanonicalFile(), fileToRelativize.getCanonicalFile())); } - public static String getRelativePath(String basePath, String pathToRelativize) throws IOException { - File baseFile = new File(basePath); - if (baseFile.isFile()) { - baseFile = baseFile.getParentFile(); - } - - return getRelativeFileInternal(baseFile.getCanonicalFile(), - new File(pathToRelativize).getCanonicalFile()); - } - static String getRelativeFileInternal(File canonicalBaseFile, File canonicalFileToRelativize) { - ArrayList<String> basePath = getPathComponents(canonicalBaseFile); - ArrayList<String> pathToRelativize = getPathComponents(canonicalFileToRelativize); + List<String> basePath = getPathComponents(canonicalBaseFile); + List<String> pathToRelativize = getPathComponents(canonicalFileToRelativize); //if the roots aren't the same (i.e. different drives on a windows machine), we can't construct a relative //path from one to the other, so just return the canonical file @@ -105,21 +98,21 @@ public class PathUtil { return sb.toString(); } - private static ArrayList<String> getPathComponents(File file) { + private static List<String> getPathComponents(File file) { ArrayList<String> path = new ArrayList<String>(); while (file != null) { File parentFile = file.getParentFile(); if (parentFile == null) { - path.add(0, file.getPath()); + path.add(file.getPath()); } else { - path.add(0, file.getName()); + path.add(file.getName()); } file = parentFile; } - return path; + return Lists.reverse(path); } } diff --git a/util/src/main/java/org/jf/util/SmaliHelpFormatter.java b/util/src/main/java/org/jf/util/SmaliHelpFormatter.java deleted file mode 100644 index 3d0137e4..00000000 --- a/util/src/main/java/org/jf/util/SmaliHelpFormatter.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * [The "BSD licence"] - * Copyright (c) 2010 Ben Gruver (JesusFreke) - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, - * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT - * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.jf.util; - -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Options; - -import java.io.PrintWriter; - -public class SmaliHelpFormatter extends HelpFormatter { - public void printHelp(String cmdLineSyntax, String header, Options options, Options debugOptions) { - super.printHelp(cmdLineSyntax, header, options, ""); - if (debugOptions != null) { - System.out.println(); - System.out.println("Debug Options:"); - PrintWriter pw = new PrintWriter(System.out); - super.printOptions(pw, getWidth(), debugOptions, getLeftPadding(), getDescPadding()); - pw.flush(); - } - } -} diff --git a/util/src/main/java/org/jf/util/StringWrapper.java b/util/src/main/java/org/jf/util/StringWrapper.java index 91808300..304c2972 100644 --- a/util/src/main/java/org/jf/util/StringWrapper.java +++ b/util/src/main/java/org/jf/util/StringWrapper.java @@ -33,9 +33,92 @@ package org.jf.util; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.io.PrintStream; +import java.text.BreakIterator; +import java.util.Iterator; public class StringWrapper { /** + * Splits the given string into lines of maximum width maxWidth. The splitting is done using the current locale's + * rules for splitting lines. + * + * @param string The string to split + * @param maxWidth The maximum length of any line + * @return An iterable of Strings containing the wrapped lines + */ + public static Iterable<String> wrapStringOnBreaks(@Nonnull final String string, final int maxWidth) { + // TODO: should we strip any trailing newlines? + final BreakIterator breakIterator = BreakIterator.getLineInstance(); + breakIterator.setText(string); + + return new Iterable<String>() { + @Override + public Iterator<String> iterator() { + return new Iterator<String>() { + private int currentLineStart = 0; + private boolean nextLineSet = false; + private String nextLine; + + @Override + public boolean hasNext() { + if (!nextLineSet) { + calculateNext(); + } + return nextLine != null; + } + + private void calculateNext() { + int lineEnd = currentLineStart; + while (true) { + lineEnd = breakIterator.following(lineEnd); + if (lineEnd == BreakIterator.DONE) { + lineEnd = breakIterator.last(); + if (lineEnd <= currentLineStart) { + nextLine = null; + nextLineSet = true; + return; + } + break; + } + + if (lineEnd - currentLineStart > maxWidth) { + lineEnd = breakIterator.preceding(lineEnd); + if (lineEnd <= currentLineStart) { + lineEnd = currentLineStart + maxWidth; + } + break; + } + + if (string.charAt(lineEnd-1) == '\n') { + nextLine = string.substring(currentLineStart, lineEnd-1); + nextLineSet = true; + currentLineStart = lineEnd; + return; + } + } + nextLine = string.substring(currentLineStart, lineEnd); + nextLineSet = true; + currentLineStart = lineEnd; + } + + @Override + public String next() { + String ret = nextLine; + nextLine = null; + nextLineSet = false; + return ret; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + /** * Splits the given string into lines using on any embedded newlines, and wrapping the text as needed to conform to * the given maximum line width. * @@ -103,4 +186,14 @@ public class StringWrapper { System.arraycopy(arr, 0, newArr, 0, arr.length); return newArr; } + + public static void printWrappedString(@Nonnull PrintStream stream, @Nonnull String string) { + printWrappedString(stream, string, ConsoleUtil.getConsoleWidth()); + } + + public static void printWrappedString(@Nonnull PrintStream stream, @Nonnull String string, int maxWidth) { + for (String str: wrapStringOnBreaks(string, maxWidth)) { + stream.println(str); + } + } } diff --git a/util/src/main/java/org/jf/util/WrappedIndentingWriter.java b/util/src/main/java/org/jf/util/WrappedIndentingWriter.java index eb1acdae..df4575bd 100644 --- a/util/src/main/java/org/jf/util/WrappedIndentingWriter.java +++ b/util/src/main/java/org/jf/util/WrappedIndentingWriter.java @@ -1,18 +1,18 @@ /* - * Copyright 2013, Google Inc. + * Copyright 2016, Google Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * - * * Redistributions of source code must retain the above copyright + * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. - * * Redistributions in binary form must reproduce the above + * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. - * * Neither the name of Google Inc. nor the names of its + * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * @@ -31,154 +31,94 @@ package org.jf.util; +import com.google.common.collect.Lists; + import java.io.FilterWriter; import java.io.IOException; import java.io.Writer; +import java.util.List; -/** - * Writer that wraps another writer and passes width-limited and - * optionally-prefixed output to its subordinate. When lines are - * wrapped they are automatically indented based on the start of the - * line. - */ -public final class WrappedIndentingWriter extends FilterWriter { - /** null-ok; optional prefix for every line */ - private final String prefix; - - /** > 0; the maximum output width */ - private final int width; +public class WrappedIndentingWriter extends FilterWriter { - /** > 0; the maximum indent */ private final int maxIndent; + private final int maxWidth; - /** >= 0; current output column (zero-based) */ - private int column; - - /** whether indent spaces are currently being collected */ - private boolean collectingIndent; - - /** >= 0; current indent amount */ - private int indent; + private int currentIndent = 0; + private final StringBuilder line = new StringBuilder(); - /** - * Constructs an instance. - * - * @param out non-null; writer to send final output to - * @param width >= 0; the maximum output width (not including - * <code>prefix</code>), or <code>0</code> for no maximum - * @param prefix non-null; the prefix for each line - */ - public WrappedIndentingWriter(Writer out, int width, String prefix) { + public WrappedIndentingWriter(Writer out, int maxIndent, int maxWidth) { super(out); + this.maxIndent = maxIndent; + this.maxWidth = maxWidth; + } - if (out == null) { - throw new NullPointerException("out == null"); + private void writeIndent() throws IOException { + for (int i=0; i<getIndent(); i++) { + write(' '); } + } - if (width < 0) { - throw new IllegalArgumentException("width < 0"); + private int getIndent() { + if (currentIndent < 0) { + return 0; } - - if (prefix == null) { - throw new NullPointerException("prefix == null"); + if (currentIndent > maxIndent) { + return maxIndent; } + return currentIndent; + } - this.width = (width != 0) ? width : Integer.MAX_VALUE; - this.maxIndent = width >> 1; - this.prefix = (prefix.length() == 0) ? null : prefix; - - bol(); + public void indent(int indent) { + currentIndent += indent; } - /** - * Constructs a no-prefix instance. - * - * @param out non-null; writer to send final output to - * @param width >= 0; the maximum output width (not including - * <code>prefix</code>), or <code>0</code> for no maximum - */ - public WrappedIndentingWriter(Writer out, int width) { - this(out, width, ""); + public void deindent(int indent) { + currentIndent -= indent; } - /** {@inheritDoc} */ - @Override - public void write(int c) throws IOException { - synchronized (lock) { - if (collectingIndent) { - if (c == ' ') { - indent++; - if (indent >= maxIndent) { - indent = maxIndent; - collectingIndent = false; - } - } else { - collectingIndent = false; - } - } + private void wrapLine() throws IOException { + List<String> wrapped = Lists.newArrayList(StringWrapper.wrapStringOnBreaks(line.toString(), maxWidth)); + out.write(wrapped.get(0), 0, wrapped.get(0).length()); + out.write('\n'); - if ((column == width) && (c != '\n')) { - out.write('\n'); - column = 0; - /* - * Note: No else, so this should fall through to the next - * if statement. - */ - } - - if (column == 0) { - if (prefix != null) { - out.write(prefix); - } - - if (!collectingIndent) { - for (int i = 0; i < indent; i++) { - out.write(' '); - } - column = indent; - } + line.replace(0, line.length(), ""); + writeIndent(); + for (int i=1; i<wrapped.size(); i++) { + if (i > 1) { + write('\n'); } + write(wrapped.get(i)); + } + } + @Override public void write(int c) throws IOException { + if (c == '\n') { + out.write(line.toString()); out.write(c); - - if (c == '\n') { - bol(); - } else { - column++; + line.replace(0, line.length(), ""); + writeIndent(); + } else { + line.append((char)c); + if (line.length() > maxWidth) { + wrapLine(); } } } - /** {@inheritDoc} */ - @Override - public void write(char[] cbuf, int off, int len) throws IOException { - synchronized (lock) { - while (len > 0) { - write(cbuf[off]); - off++; - len--; - } + @Override public void write(char[] cbuf, int off, int len) throws IOException { + for (int i=0; i<len; i++) { + write(cbuf[i+off]); } } - /** {@inheritDoc} */ - @Override - public void write(String str, int off, int len) throws IOException { - synchronized (lock) { - while (len > 0) { - write(str.charAt(off)); - off++; - len--; - } + @Override public void write(String str, int off, int len) throws IOException { + for (int i=0; i<len; i++) { + write(str.charAt(i+off)); } } - /** - * Indicates that output is at the beginning of a line. - */ - private void bol() { - column = 0; - collectingIndent = (maxIndent != 0); - indent = 0; + @Override public void flush() throws IOException { + out.write(line.toString()); + line.replace(0, line.length(), ""); } } diff --git a/util/src/main/java/org/jf/util/jcommander/ColonParameterSplitter.java b/util/src/main/java/org/jf/util/jcommander/ColonParameterSplitter.java new file mode 100644 index 00000000..eb628aff --- /dev/null +++ b/util/src/main/java/org/jf/util/jcommander/ColonParameterSplitter.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util.jcommander; + +import com.beust.jcommander.converters.IParameterSplitter; + +import java.util.Arrays; +import java.util.List; + +/** + * A JCommander parameter splitter that splits a parameter value by colon + */ +public class ColonParameterSplitter implements IParameterSplitter { + @Override + public List<String> split(String value) { + return Arrays.asList(value.split(":")); + } +} diff --git a/util/src/main/java/org/jf/util/jcommander/Command.java b/util/src/main/java/org/jf/util/jcommander/Command.java new file mode 100644 index 00000000..8fac0fab --- /dev/null +++ b/util/src/main/java/org/jf/util/jcommander/Command.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util.jcommander; + +import com.beust.jcommander.JCommander; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.jf.util.ConsoleUtil; + +import javax.annotation.Nonnull; +import java.util.List; + +public abstract class Command { + + @Nonnull + protected final List<JCommander> commandAncestors; + + public Command(@Nonnull List<JCommander> commandAncestors) { + this.commandAncestors = commandAncestors; + } + + public void usage() { + System.out.println(new HelpFormatter() + .width(ConsoleUtil.getConsoleWidth()) + .format(getCommandHierarchy())); + } + + protected void setupCommand(JCommander jc) { + } + + protected JCommander getJCommander() { + JCommander parentJc = Iterables.getLast(commandAncestors); + return parentJc.getCommands().get(this.getClass().getAnnotation(ExtendedParameters.class).commandName()); + } + + public List<JCommander> getCommandHierarchy() { + List<JCommander> commandHierarchy = Lists.newArrayList(commandAncestors); + commandHierarchy.add(getJCommander()); + return commandHierarchy; + } + + public abstract void run(); +} diff --git a/util/src/main/java/org/jf/util/jcommander/ExtendedCommands.java b/util/src/main/java/org/jf/util/jcommander/ExtendedCommands.java new file mode 100644 index 00000000..209d94e2 --- /dev/null +++ b/util/src/main/java/org/jf/util/jcommander/ExtendedCommands.java @@ -0,0 +1,150 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util.jcommander; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.ParameterDescription; +import com.beust.jcommander.Parameterized; +import com.beust.jcommander.Parameters; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Field; + +/** + * Utilities related to "extended" commands - JCommander commands with additional information + */ +public class ExtendedCommands { + + @Nonnull + private static ExtendedParameters getExtendedParameters(Object command) { + ExtendedParameters anno = command.getClass().getAnnotation(ExtendedParameters.class); + if (anno == null) { + throw new IllegalStateException("All extended commands should have an ExtendedParameters annotation: " + + command.getClass().getCanonicalName()); + } + return anno; + } + + @Nonnull + public static String commandName(JCommander jc) { + return getExtendedParameters(jc.getObjects().get(0)).commandName(); + } + + @Nonnull + public static String commandName(Object command) { + return getExtendedParameters(command).commandName(); + } + + @Nonnull + public static String[] commandAliases(JCommander jc) { + return commandAliases(jc.getObjects().get(0)); + } + + @Nonnull + public static String[] commandAliases(Object command) { + return getExtendedParameters(command).commandAliases(); + } + + public static boolean includeParametersInUsage(JCommander jc) { + return includeParametersInUsage(jc.getObjects().get(0)); + } + + public static boolean includeParametersInUsage(Object command) { + return getExtendedParameters(command).includeParametersInUsage(); + } + + @Nonnull + public static String postfixDescription(JCommander jc) { + return postfixDescription(jc.getObjects().get(0)); + } + + @Nonnull + public static String postfixDescription(Object command) { + return getExtendedParameters(command).postfixDescription(); + } + + public static void addExtendedCommand(JCommander jc, Command command) { + jc.addCommand(commandName(command), command, commandAliases(command)); + command.setupCommand(command.getJCommander()); + } + + @Nonnull + public static String[] parameterArgumentNames(ParameterDescription parameterDescription) { + Parameterized parameterized = parameterDescription.getParameterized(); + + Class cls = parameterDescription.getObject().getClass(); + Field field = null; + while (cls != Object.class) { + try { + field = cls.getDeclaredField(parameterized.getName()); + } catch (NoSuchFieldException ex) { + cls = cls.getSuperclass(); + continue; + } + break; + } + + assert field != null; + ExtendedParameter extendedParameter = field.getAnnotation(ExtendedParameter.class); + if (extendedParameter != null) { + return extendedParameter.argumentNames(); + } + + return new String[0]; + } + + @Nullable + public static JCommander getSubcommand(JCommander jc, String commandName) { + if (jc.getCommands().containsKey(commandName)) { + return jc.getCommands().get(commandName); + } else { + for (JCommander command : jc.getCommands().values()) { + for (String alias: commandAliases(command)) { + if (commandName.equals(alias)) { + return command; + } + } + } + } + return null; + } + + @Nullable + public static String getCommandDescription(@Nonnull JCommander jc) { + Parameters parameters = jc.getObjects().get(0).getClass().getAnnotation(Parameters.class); + if (parameters == null) { + return null; + } + return parameters.commandDescription(); + } +} diff --git a/util/src/main/java/org/jf/util/jcommander/ExtendedParameter.java b/util/src/main/java/org/jf/util/jcommander/ExtendedParameter.java new file mode 100644 index 00000000..81f78c22 --- /dev/null +++ b/util/src/main/java/org/jf/util/jcommander/ExtendedParameter.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util.jcommander; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ExtendedParameter { + String[] argumentNames(); +} diff --git a/util/src/main/java/org/jf/util/jcommander/ExtendedParameters.java b/util/src/main/java/org/jf/util/jcommander/ExtendedParameters.java new file mode 100644 index 00000000..965d2b2a --- /dev/null +++ b/util/src/main/java/org/jf/util/jcommander/ExtendedParameters.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util.jcommander; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ExtendedParameters { + boolean includeParametersInUsage() default false; + String commandName(); + String[] commandAliases() default { }; + String postfixDescription() default ""; +} diff --git a/util/src/main/java/org/jf/util/jcommander/HelpFormatter.java b/util/src/main/java/org/jf/util/jcommander/HelpFormatter.java new file mode 100644 index 00000000..e807d5fe --- /dev/null +++ b/util/src/main/java/org/jf/util/jcommander/HelpFormatter.java @@ -0,0 +1,316 @@ +/* + * Copyright 2016, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.jf.util.jcommander; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.ParameterDescription; +import com.beust.jcommander.Parameters; +import com.beust.jcommander.internal.Lists; +import com.google.common.base.Joiner; +import com.google.common.collect.Iterables; +import org.jf.util.WrappedIndentingWriter; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HelpFormatter { + + private int width = 80; + + @Nonnull + public HelpFormatter width(int width) { + this.width = width; + return this; + } + + @Nonnull + private static ExtendedParameters getExtendedParameters(JCommander jc) { + ExtendedParameters anno = jc.getObjects().get(0).getClass().getAnnotation(ExtendedParameters.class); + if (anno == null) { + throw new IllegalStateException("All commands should have an ExtendedParameters annotation"); + } + return anno; + } + + @Nonnull + private static List<String> getCommandAliases(JCommander jc) { + return Lists.newArrayList(getExtendedParameters(jc).commandAliases()); + } + + private static boolean includeParametersInUsage(@Nonnull JCommander jc) { + return getExtendedParameters(jc).includeParametersInUsage(); + } + + @Nonnull + private static String getPostfixDescription(@Nonnull JCommander jc) { + return getExtendedParameters(jc).postfixDescription(); + } + + private int getParameterArity(ParameterDescription param) { + if (param.getParameter().arity() > 0) { + return param.getParameter().arity(); + } + Class<?> type = param.getParameterized().getType(); + if ((type == boolean.class || type == Boolean.class)) { + return 0; + } + return 1; + } + + private List<ParameterDescription> getSortedParameters(JCommander jc) { + List<ParameterDescription> parameters = Lists.newArrayList(jc.getParameters()); + + final Pattern pattern = Pattern.compile("^-*(.*)$"); + + Collections.sort(parameters, new Comparator<ParameterDescription>() { + @Override public int compare(ParameterDescription o1, ParameterDescription o2) { + String s1; + Matcher matcher = pattern.matcher(o1.getParameter().names()[0]); + if (matcher.matches()) { + s1 = matcher.group(1); + } else { + throw new IllegalStateException(); + } + + String s2; + matcher = pattern.matcher(o2.getParameter().names()[0]); + if (matcher.matches()) { + s2 = matcher.group(1); + } else { + throw new IllegalStateException(); + } + + return s1.compareTo(s2); + } + }); + return parameters; + } + + @Nonnull + public String format(@Nonnull JCommander... jc) { + return format(Arrays.asList(jc)); + } + + @Nonnull + public String format(@Nonnull List<JCommander> commandHierarchy) { + try { + StringWriter stringWriter = new StringWriter(); + WrappedIndentingWriter writer = new WrappedIndentingWriter(stringWriter, width - 5, width); + + JCommander leafJc = Iterables.getLast(commandHierarchy); + + writer.write("usage:"); + writer.indent(2); + + for (JCommander jc: commandHierarchy) { + writer.write(" "); + writer.write(ExtendedCommands.commandName(jc)); + } + + if (includeParametersInUsage(leafJc)) { + for (ParameterDescription param : leafJc.getParameters()) { + if (!param.getParameter().hidden()) { + writer.write(" ["); + writer.write(param.getParameter().getParameter().names()[0]); + writer.write("]"); + } + } + } else { + if (!leafJc.getParameters().isEmpty()) { + writer.write(" [<options>]"); + } + } + + if (!leafJc.getCommands().isEmpty()) { + writer.write(" [<command [<args>]]"); + } + + if (leafJc.getMainParameter() != null) { + String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameter()); + if (argumentNames.length == 0) { + writer.write(" <args>"); + } else { + String argumentName = argumentNames[0]; + boolean writeAngleBrackets = !argumentName.startsWith("<") && !argumentName.startsWith("["); + writer.write(" "); + if (writeAngleBrackets) { + writer.write("<"); + } + writer.write(argumentNames[0]); + if (writeAngleBrackets) { + writer.write(">"); + } + } + } + + writer.deindent(2); + + String commandDescription = ExtendedCommands.getCommandDescription(leafJc); + if (commandDescription != null) { + writer.write("\n"); + writer.write(commandDescription); + } + + if (!leafJc.getParameters().isEmpty() || leafJc.getMainParameter() != null) { + writer.write("\n\nOptions:"); + writer.indent(2); + for (ParameterDescription param : getSortedParameters(leafJc)) { + if (!param.getParameter().hidden()) { + writer.write("\n"); + writer.indent(4); + if (!param.getNames().isEmpty()) { + writer.write(Joiner.on(',').join(param.getParameter().names())); + } + if (getParameterArity(param) > 0) { + String[] argumentNames = ExtendedCommands.parameterArgumentNames(param); + for (int i = 0; i < getParameterArity(param); i++) { + writer.write(" "); + if (i < argumentNames.length) { + writer.write("<"); + writer.write(argumentNames[i]); + writer.write(">"); + } else { + writer.write("<arg>"); + } + } + } + if (param.getDescription() != null && !param.getDescription().isEmpty()) { + writer.write(" - "); + writer.write(param.getDescription()); + } + if (param.getDefault() != null) { + String defaultValue = null; + if (param.getParameterized().getType() == Boolean.class || + param.getParameterized().getType() == Boolean.TYPE) { + if ((Boolean)param.getDefault()) { + defaultValue = "True"; + } + } else if (List.class.isAssignableFrom(param.getParameterized().getType())) { + if (!((List)param.getDefault()).isEmpty()) { + defaultValue = param.getDefault().toString(); + } + } else { + defaultValue = param.getDefault().toString(); + } + if (defaultValue != null) { + writer.write(" (default: "); + writer.write(defaultValue); + writer.write(")"); + } + } + writer.deindent(4); + } + } + + if (leafJc.getMainParameter() != null) { + String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameter()); + writer.write("\n"); + writer.indent(4); + if (argumentNames.length > 0) { + writer.write("<"); + writer.write(argumentNames[0]); + writer.write(">"); + } else { + writer.write("<args>"); + } + + if (leafJc.getMainParameterDescription() != null) { + writer.write(" - "); + writer.write(leafJc.getMainParameterDescription()); + } + writer.deindent(4); + } + writer.deindent(2); + } + + if (!leafJc.getCommands().isEmpty()) { + writer.write("\n\nCommands:"); + writer.indent(2); + + + List<Entry<String, JCommander>> entryList = Lists.newArrayList(leafJc.getCommands().entrySet()); + Collections.sort(entryList, new Comparator<Entry<String, JCommander>>() { + @Override public int compare(Entry<String, JCommander> o1, Entry<String, JCommander> o2) { + return o1.getKey().compareTo(o2.getKey()); + } + }); + + for (Entry<String, JCommander> entry : entryList) { + String commandName = entry.getKey(); + JCommander command = entry.getValue(); + + Object arg = command.getObjects().get(0); + Parameters parametersAnno = arg.getClass().getAnnotation(Parameters.class); + if (!parametersAnno.hidden()) { + writer.write("\n"); + writer.indent(4); + writer.write(commandName); + List<String> aliases = getCommandAliases(command); + if (!aliases.isEmpty()) { + writer.write("("); + writer.write(Joiner.on(',').join(aliases)); + writer.write(")"); + } + + String commandDesc = leafJc.getCommandDescription(commandName); + if (commandDesc != null) { + writer.write(" - "); + writer.write(commandDesc); + } + writer.deindent(4); + } + } + writer.deindent(2); + } + + String postfixDescription = getPostfixDescription(leafJc); + if (!postfixDescription.isEmpty()) { + writer.write("\n\n"); + writer.write(postfixDescription); + } + + writer.flush(); + + return stringWriter.getBuffer().toString(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/util/src/test/java/org/jf/util/StringWrapperTest.java b/util/src/test/java/org/jf/util/StringWrapperTest.java index 64dca33e..94c79142 100644 --- a/util/src/test/java/org/jf/util/StringWrapperTest.java +++ b/util/src/test/java/org/jf/util/StringWrapperTest.java @@ -31,11 +31,35 @@ package org.jf.util; +import com.google.common.collect.Lists; import org.junit.Assert; import org.junit.Test; +import java.util.List; + public class StringWrapperTest { @Test + public void testWrapStringByWords() { + validateResult2(new String[]{"abc", "abcdef", "abcdef"}, + "abc\nabcdefabcdef", 6); + + validateResult2(new String[]{"abc", "abcdef", " ", "abcdef"}, + "abc\nabcdef abcdef", 6); + + validateResult2(new String[]{"abc", "abcde ", "fabcde", "f"}, + "abc\nabcde fabcdef", 6); + + validateResult2(new String[]{"abc def ghi ", "kjl mon pqr ", "stu vwx yz"}, + "abc def ghi kjl mon pqr stu vwx yz", 14); + + validateResult2(new String[]{"abcdefg", "hikjlmo", "npqrstu", "vwxyz"}, + "abcdefghikjlmonpqrstuvwxyz", 7); + + validateResult2(new String[]{"abc", "defhig"}, + "abc\ndefhig", 20); + } + + @Test public void testWrapString() { validateResult( new String[]{"abc", "abcdef", "abcdef"}, @@ -115,4 +139,15 @@ public class StringWrapperTest { Assert.assertEquals(expected[i], actual[i]); } } + + public static void validateResult2(String[] expected, String textToWrap, int maxWidth) { + List<String> result = Lists.newArrayList(StringWrapper.wrapStringOnBreaks(textToWrap, maxWidth)); + + Assert.assertEquals(expected.length, result.size()); + int i; + for (i=0; i<result.size(); i++) { + Assert.assertTrue(i < expected.length); + Assert.assertEquals(expected[i], result.get(i)); + } + } } |