diff --git a/baksmali/build.gradle b/baksmali/build.gradle index f3a14b19..3c8c4762 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,7 @@ 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[]); }' keepclassmembers 'enum * { public static **[] values(); public static ** valueOf(java.lang.String); }' dontwarn 'com.google.common.**' @@ -100,3 +100,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 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 findFieldsSetInStaticConstructor() { + private static HashSet findFieldsSetInStaticConstructor(@Nonnull ClassDef classDef) { HashSet fieldsSetInStaticConstructor = new HashSet(); 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 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 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 { 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..9332111b 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 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 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,7 @@ public class MethodDefinition { }); } - if (!classDef.options.noAccessorComments && (instruction instanceof ReferenceInstruction)) { + if (classDef.options.accessorComments && (instruction instanceof ReferenceInstruction)) { Opcode opcode = instruction.getOpcode(); if (opcode.referenceType == ReferenceType.METHOD) { @@ -493,7 +493,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 +597,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..83c4b13f --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java @@ -0,0 +1,137 @@ +/* + * 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; + +public class AnalysisArguments { + @Parameter(names = {"-a", "--api"}, + description = "The numeric api level of the file being disassembled.") + @ExtendedParameter(argumentNames = "api") + public int apiLevel = 15; + + @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 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 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 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, 0); + } + + @Nonnull + public ClassPath loadClassPathForDexFile(@Nonnull File dexFileDir, @Nonnull DexFile dexFile, + boolean checkPackagePrivateAccess, int oatVersion) + throws IOException { + ClassPathResolver resolver; + + if (dexFile instanceof OatDexFile) { + checkPackagePrivateAccess = true; + } + + if (classPathDirectories == null || classPathDirectories.size() == 0) { + classPathDirectories = Lists.newArrayList(dexFileDir.getPath()); + } + + List 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, apiLevel); + } 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.of(), ImmutableList.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 similarity index 60% rename from baksmali/src/main/java/org/jf/baksmali/baksmali.java rename to baksmali/src/main/java/org/jf/baksmali/Baksmali.java index 50607340..a2fc4112 100644 --- a/baksmali/src/main/java/org/jf/baksmali/baksmali.java +++ b/baksmali/src/main/java/org/jf/baksmali/Baksmali.java @@ -28,105 +28,20 @@ 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 java.io.*; import java.util.List; -import java.util.Map.Entry; 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 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 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; - } - } - } - - 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 class Baksmali { + public static boolean disassembleDexFile(DexFile dexFile, File outputDir, int jobs, final BaksmaliOptions options) { //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,13 +49,9 @@ public class baksmali { //may still change of course List classDefs = Ordering.natural().sortedCopy(dexFile.getClasses()); - if (!options.noAccessorComments) { - options.syntheticAccessorResolver = new SyntheticAccessorResolver(dexFile.getOpcodes(), classDefs); - } + final ClassFileNameHandler fileNameHandler = new ClassFileNameHandler(outputDir, ".smali"); - final ClassFileNameHandler fileNameHandler = new ClassFileNameHandler(outputDirectoryFile, ".smali"); - - ExecutorService executor = Executors.newFixedThreadPool(options.jobs); + ExecutorService executor = Executors.newFixedThreadPool(jobs); List> tasks = Lists.newArrayList(); for (final ClassDef classDef: classDefs) { @@ -174,7 +85,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 similarity index 55% rename from baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java rename to 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 bootClassPathDirs = Lists.newArrayList(); - - public List bootClassPathEntries = Lists.newArrayList(); - public List extraClassPathEntries = Lists.newArrayList(); - - public Map resourceIdFileEntries = new HashMap(); - public Map resourceIds = new HashMap(); - - 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 Map resourceIds = new HashMap(); + public InlineMethodResolver inlineResolver = null; + public ClassPath classPath = 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 resourceFiles) throws SAXException, IOException { + for (Map.Entry 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 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..7117bb25 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java @@ -0,0 +1,146 @@ +/* + * 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(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 inputList = Lists.newArrayList(); + + protected File inputFile; + protected String inputEntry; + protected DexBackedDexFile dexFile; + + public DexInputCommand(@Nonnull List 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. + * @param opcodes The set of opcodes to load the dex file with. + */ + protected void loadDexFile(@Nonnull String input, Opcodes opcodes) { + 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); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } else { + try { + dexFile = DexFileFactory.loadDexFile(file, opcodes); + } 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..1ce49523 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java @@ -0,0 +1,283 @@ +/* + * 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.Opcodes; +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 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 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; + + public DisassembleCommand(@Nonnull List 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, Opcodes.getDefault()); + + 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())) { + 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 resourceFiles = Maps.newHashMap(); + + assert (resourceIdFiles.size() % 2) == 0; + for (int i=0; i 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, Opcodes.getDefault()); + + try { + dump(dexFile, System.out, apiLevel); + } 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. + * @param apiLevel The api level to use when dumping the dex file + * + * @throws IOException + */ + public static void dump(@Nonnull DexBackedDexFile dexFile, @Nonnull OutputStream output, int apiLevel) + 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 commandAncestors) { + super(commandAncestors); + } + + @Parameter(description = "If specified, show the detailed usage information for the given commands") + @ExtendedParameter(argumentNames = "commands") + private List 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 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 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 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 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..6c6ca64b --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.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.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import org.jf.dexlib2.Opcodes; +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 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, Opcodes.getDefault()); + + 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 commandAncestors) { + super(commandAncestors); + } + + @Override protected void setupCommand(JCommander jc) { + List 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 inputList = Lists.newArrayList(); + + public ListDependenciesCommand(@Nonnull List 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 inputList = Lists.newArrayList(); + + public ListDexCommand(@Nonnull List 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 entries; + try { + MultiDexContainer 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..68e2050b --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java @@ -0,0 +1,121 @@ +/* + * 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.Opcodes; +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 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, Opcodes.getDefault()); + BaksmaliOptions options = getOptions(); + + try { + for (ClassDef classDef: dexFile.getClasses()) { + ClassProto classProto = (ClassProto) options.classPath.getClass(classDef); + SparseArray fields = classProto.getInstanceFields(); + String className = "Class " + classDef.getType() + " : " + fields.size() + " instance fields\n"; + System.out.write(className.getBytes()); + for (int i=0;i 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 commands; + + public ListHelpCommand(@Nonnull List 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 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 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..3403bbf9 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.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.baksmali; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import org.jf.dexlib2.Opcodes; +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 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, Opcodes.getDefault()); + + 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 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 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..ed88c123 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java @@ -0,0 +1,158 @@ +/* + * 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.Opcodes; +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 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 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, Opcodes.getDefault()); + + 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 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 = analysisArguments.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 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.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 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 2d6ed8c4..00000000 --- a/baksmali/src/main/java/org/jf/baksmali/main.java +++ /dev/null @@ -1,612 +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", - "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 syntax instead of the p 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" + - " (.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 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 { // api >= 21 - // TODO: verify, add new ones? - 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"); - } - } -} diff --git a/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java b/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java index 2bb04dda..512e1607 100644 --- a/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java +++ b/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java @@ -36,6 +36,7 @@ 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.iface.ClassDef; import org.jf.dexlib2.iface.DexFile; @@ -85,14 +86,14 @@ public class AnalysisTest { public void runTest(String test, boolean registerInfo) 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.registerInfo = BaksmaliOptions.ALL | BaksmaliOptions.FULLMERGE; 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/build.gradle b/build.gradle index 56aaa3f6..c9c8beb1 100644 --- a/build.gradle +++ b/build.gradle @@ -101,14 +101,15 @@ 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.+', 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: 'com.beust:jcommander:1.48' ] } diff --git a/dexlib2/OatVersions.txt b/dexlib2/OatVersions.txt index 8aa9ea96..abb4a83f 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,27 @@ 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 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..f833ac85 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java @@ -31,14 +31,19 @@ package org.jf.dexlib2; -import com.google.common.base.MoreObjects; -import com.google.common.io.ByteStreams; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; 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.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 +51,45 @@ 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); + 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 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); - } - - @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)); - } - - @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 - } - } + public static DexBackedDexFile loadDexFile(@Nonnull File file, @Nonnull Opcodes opcodes) throws IOException { + if (!file.exists()) { + throw new DexFileNotFoundException("%s does not exist", file.getName()); } - InputStream inputStream = new BufferedInputStream(new FileInputStream(dexFile)); + + try { + 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(file)); try { try { return DexBackedDexFile.fromInputStream(opcodes, inputStream); @@ -127,14 +97,15 @@ 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); @@ -150,71 +121,181 @@ public final class DexFileFactory { List 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; - } - } - - if (!dexEntry.contains("/")) { - for (OatDexFile oatDexFile : oatFile.getDexFiles()) { - File oatEntryFile = new File(oatDexFile.filename); - if (oatEntryFile.getName().equals(dexEntry)) { - return oatDexFile; - } - } - } - - throw new DexFileNotFound("oat file %s does not contain a dex file named %s", - dexFile.getName(), dexEntry); - } + return oatDexFiles.get(0); } } 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, 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); + } catch (NotAnOatFileException ex) { + // just eat it + } + + if (oatFile != null) { + if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) { + throw new UnsupportedOatVersionException(oatFile); + } + + List 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 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 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); + } 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 DexFileNotFound(@Nullable Throwable cause, @Nullable String message, Object... formatArgs) { - super(cause, message, formatArgs); - } - - public DexFileNotFound(@Nullable String message, Object... formatArgs) { + public static class DexFileNotFoundException extends ExceptionWithContext { + public DexFileNotFoundException(@Nullable String message, Object... formatArgs) { super(message, formatArgs); } } - public static class MultipleDexFilesException extends ExceptionWithContext { - @Nonnull public final OatFile oatFile; - - public MultipleDexFilesException(@Nonnull OatFile oatFile) { - super("Oat file has multiple dex files."); - this.oatFile = oatFile; - } - } - public static class UnsupportedOatVersionException extends ExceptionWithContext { @Nonnull public final OatFile oatFile; @@ -223,4 +304,155 @@ public final class DexFileFactory { this.oatFile = oatFile; } } + + public static class MultipleMatchingDexEntriesException extends ExceptionWithContext { + public MultipleMatchingDexEntriesException(@Nonnull String message, Object... formatArgs) { + super(String.format(message, formatArgs)); + } + } + + public static class UnsupportedFileTypeException extends ExceptionWithContext { + public UnsupportedFileTypeException(@Nonnull String message, Object... formatArgs) { + super(String.format(message, formatArgs)); + } + } + + /** + * 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); + } + + /** + * 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; + } + + 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 dexContainer; + + public DexEntryFinder(@Nonnull String filename, + @Nonnull MultiDexContainer 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 fullMatches = Lists.newArrayList(); + List fullEntries = Lists.newArrayList(); + List partialMatches = Lists.newArrayList(); + List 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 { + 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 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; + } + } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java b/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java index 138c6c63..1d7a12d9 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 apiRange; @Nonnull public final Range artVersionRange; diff --git a/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java b/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java index 17f80132..c1e40ad2 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java @@ -39,6 +39,10 @@ import javax.annotation.Nullable; import java.util.EnumMap; import java.util.HashMap; +import static org.jf.dexlib2.VersionMap.NO_VERSION; +import static org.jf.dexlib2.VersionMap.mapApiToArtVersion; +import static org.jf.dexlib2.VersionMap.mapArtVersionToApi; + public class Opcodes { /** @@ -52,37 +56,36 @@ public class Opcodes { @Nonnull public static Opcodes forApi(int api) { - return new Opcodes(api, VersionMap.mapApiToArtVersion(api), false); - } - - @Nonnull - public static Opcodes forApi(int api, boolean experimental) { - return new Opcodes(api, VersionMap.mapApiToArtVersion(api), 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(VersionMap.mapArtVersionToApi(artVersion), 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) { - this.api = api; - this.artVersion = artVersion; + if (api >= 21) { + this.api = api; + this.artVersion = mapApiToArtVersion(api); + } else if (artVersion >= 0 && artVersion < 39) { + this.api = mapArtVersionToApi(artVersion); + this.artVersion = artVersion; + } else { + this.api = api; + this.artVersion = artVersion; + } opcodeValues = new EnumMap(Opcode.class); opcodesByName = Maps.newHashMap(); @@ -104,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; } @@ -142,6 +145,6 @@ public class Opcodes { } public boolean isArt() { - return artVersion != VersionMap.NO_VERSION; + return artVersion != NO_VERSION; } } diff --git a/dexlib2/src/main/java/org/jf/dexlib2/VersionMap.java b/dexlib2/src/main/java/org/jf/dexlib2/VersionMap.java index 42802bc0..e0e1a6bb 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/VersionMap.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/VersionMap.java @@ -35,16 +35,38 @@ public class VersionMap { public static final int NO_VERSION = -1; public static int mapArtVersionToApi(int artVersion) { - // TODO: implement this - return 20; + if (artVersion >= 79) { + return 24; + } + if (artVersion >= 64) { + return 23; + } + if (artVersion >= 45) { + return 22; + } + if (artVersion >= 39) { + return 21; + } + return 19; } public static int mapApiToArtVersion(int api) { - // TODO: implement this - if (api < 20) { - return NO_VERSION; - } else { - return 56; + switch (api) { + case 19: + case 20: + return 7; + case 21: + return 39; + case 22: + return 45; + case 23: + return 64; + case 24: + return 79; } + if (api > 24) { + return 79; + } + return NO_VERSION; } } 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..111913cc 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java @@ -54,7 +54,7 @@ public class AnalyzedInstruction implements Comparable { /** * The actual instruction */ - @Nullable + @Nonnull protected Instruction instruction; /** @@ -65,21 +65,25 @@ public class AnalyzedInstruction implements Comparable { /** * Instructions that can pass on execution to this one during normal execution */ + @Nonnull protected final TreeSet predecessors = new TreeSet(); /** * Instructions that can execution could pass on to next during normal execution */ + @Nonnull protected final LinkedList successors = new LinkedList(); /** * 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 +98,8 @@ public class AnalyzedInstruction implements Comparable { */ 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 +154,17 @@ public class AnalyzedInstruction implements Comparable { instruction = originalInstruction; } - public int getSuccessorCount() { - return successors.size(); - } - - public List getSuccesors() { + @Nonnull + public List getSuccessors() { return Collections.unmodifiableList(successors); } + @Nonnull public Instruction getInstruction() { return instruction; } + @Nonnull public Instruction getOriginalInstruction() { return originalInstruction; } @@ -184,11 +187,7 @@ public class AnalyzedInstruction implements Comparable { if (predecessors.size() == 0) { return false; } - - if (predecessors.first().instructionIndex == -1) { - return true; - } - return false; + return predecessors.first().instructionIndex == -1; } /* @@ -237,6 +236,7 @@ public class AnalyzedInstruction implements Comparable { * @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 +249,10 @@ public class AnalyzedInstruction implements Comparable { } } } + if (mergedRegisterType == null) { + // This is a start-of-method or unreachable instruction. + throw new IllegalStateException(); + } return mergedRegisterType; } /** @@ -275,10 +279,10 @@ public class AnalyzedInstruction implements Comparable { * * 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 */ @@ -309,7 +313,7 @@ public class AnalyzedInstruction implements Comparable { } protected boolean isInvokeInit() { - if (instruction == null || !instruction.getOpcode().canInitializeReference()) { + if (!instruction.getOpcode().canInitializeReference()) { return false; } @@ -364,10 +368,10 @@ public class AnalyzedInstruction implements Comparable { return false; } - if (instruction.getOpcode() == Opcode.IF_EQZ || instruction.getOpcode() == Opcode.IF_NEZ) { + if (getPredecessorCount() == 1 && (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())) { @@ -421,7 +425,7 @@ public class AnalyzedInstruction implements Comparable { 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..0136374c 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; @@ -114,7 +104,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 +154,6 @@ public class ClassPath { return checkPackagePrivateAccess; } - @Nonnull - public static ClassPath fromClassPath(Iterable classPathDirs, Iterable classPath, DexFile dexFile, - int api, boolean experimental) { - return fromClassPath(classPathDirs, classPath, dexFile, api, api == 17, experimental); - } - - @Nonnull - public static ClassPath fromClassPath(Iterable classPathDirs, Iterable classPath, DexFile dexFile, - int api, boolean checkPackagePrivateAccess, boolean experimental) { - List providers = Lists.newArrayList(); - - int oatVersion = NOT_ART; - - for (String classPathEntry: classPath) { - List 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 classPathDirs, Iterable classPath, DexFile dexFile, - int api, boolean checkPackagePrivateAccess, boolean experimental, - int oatVersion) { - List providers = Lists.newArrayList(); - - for (String classPathEntry: classPath) { - List 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 loadClassPathEntry(@Nonnull Iterable 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 fieldInstructionMapperSupplier = Suppliers.memoize( new Supplier() { @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..82525bb9 --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java @@ -0,0 +1,442 @@ +/* + * 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 classPathDirs; + private final Opcodes opcodes; + + private final Set loadedFiles = Sets.newHashSet(); + private final List 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 bootClassPathDirs, @Nonnull List bootClassPathEntries, + @Nonnull List extraClassPathEntries, @Nonnull DexFile dexFile) + throws IOException { + this(bootClassPathDirs, bootClassPathEntries, extraClassPathEntries, dexFile, dexFile.getOpcodes().api); + } + + /** + * 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 + * @param apiLevel The api level of the device. This is used to select an appropriate set of boot classpath entries. + * @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 bootClassPathDirs, @Nonnull List extraClassPathEntries, + @Nonnull DexFile dexFile, int apiLevel) + throws IOException { + this(bootClassPathDirs, null, extraClassPathEntries, dexFile, apiLevel); + } + + private ClassPathResolver(@Nonnull List bootClassPathDirs, @Nullable List bootClassPathEntries, + @Nonnull List extraClassPathEntries, @Nonnull DexFile dexFile, int apiLevel) + throws IOException { + this.classPathDirs = bootClassPathDirs; + opcodes = dexFile.getOpcodes(); + + if (bootClassPathEntries == null) { + bootClassPathEntries = getDefaultBootClassPath(dexFile, apiLevel); + } + + 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) { + 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 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 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 pathComponents = splitDevicePath(entry); + Joiner pathJoiner = Joiner.on(File.pathSeparatorChar); + + for (String directory: classPathDirs) { + File directoryFile = new File(directory); + if (!directoryFile.exists()) { + // TODO: print a warning in the baksmali frontend before we get here + continue; + } + + for (int i=0; i container; + try { + container = DexFileFactory.loadDexContainer(entryFile, opcodes); + } catch (UnsupportedFileTypeException ex) { + throw new ResolveException(ex); + } + + List entryNames = container.getDexEntryNames(); + + if (entryNames.size() == 0) { + throw new NoDexException("%s contains no dex file"); + } + + loadedFiles.add(entryFile); + + for (String entryName: entryNames) { + classProviders.add(new DexClassProvider(container.getEntry(entryName))); + } + + if (loadOatDependencies && container instanceof OatFile) { + List 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 splitDevicePath(@Nonnull String path) { + return Lists.newArrayList(Splitter.on('/').split(path)); + } + + private void loadOatDependencies(@Nonnull File directory, @Nonnull List 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 getDefaultBootClassPath(@Nonnull DexFile dexFile, int apiLevel) { + if (dexFile instanceof OatFile.OatDexFile) { + List bcp = ((OatDexFile)dexFile).getContainer().getBootClassPath(); + if (!bcp.isEmpty()) { + for (int i=0; i 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> interfacesSupplier = + private final Supplier> preDefaultMethodInterfaceSupplier = Suppliers.memoize(new Supplier>() { @Override public LinkedHashMap get() { Set unresolvedInterfaces = new HashSet(0); @@ -149,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) { @@ -159,6 +166,7 @@ public class ClassProto implements TypeProto { } } } catch (UnresolvedClassException ex) { + interfaces.put(type, null); unresolvedInterfaces.add(type); interfacesFullyResolved = false; } @@ -197,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> postDefaultMethodInterfaceSupplier = + Suppliers.memoize(new Supplier>() { + @Override public LinkedHashMap get() { + Set unresolvedInterfaces = new HashSet(0); + LinkedHashMap 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 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 getUnresolvedInterfaces() { if (unresolvedInterfaces == null) { @@ -379,7 +452,10 @@ public class ClassProto implements TypeProto { } public int findMethodIndexInVtable(@Nonnull MethodReference method) { - List vtable = getVtable(); + return findMethodIndexInVtable(getVtable(), method); + } + + private int findMethodIndexInVtable(@Nonnull List vtable, MethodReference method) { for (int i=0; i getInstanceFields() { + private int findMethodIndexInVtableReverse(@Nonnull List 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 getInstanceFields() { if (classPath.isArt()) { return artInstanceFieldsSupplier.get(); } else { @@ -440,9 +529,7 @@ public class ClassProto implements TypeProto { ClassProto superclass = null; if (superclassType != null) { superclass = (ClassProto) classPath.getClass(superclassType); - if (superclass != null) { - startFieldOffset = superclass.getNextFieldOffset(); - } + startFieldOffset = superclass.getNextFieldOffset(); } int fieldIndexMod; @@ -530,13 +617,11 @@ 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; + if (fieldOffset % 8 != 0) { + assert fieldOffset % 8 == 4; + fieldOffset += 4; } + gotDouble = true; } instanceFields.append(fieldOffset, field); @@ -574,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; @@ -584,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; @@ -778,12 +863,18 @@ public class ClassProto implements TypeProto { throw new ExceptionWithContext("Invalid type: %s", type); } - @Nonnull List getVtable() { - return vtableSupplier.get(); + @Nonnull public List 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> vtableSupplier = Suppliers.memoize(new Supplier>() { + @Nonnull private final Supplier> preDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier>() { @Override public List get() { List vtable = Lists.newArrayList(); @@ -812,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 interfaces = getDirectInterfaces(); + for (ClassDef interfaceDef: interfaces) { List 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 localMethods, - @Nonnull List vtable, boolean replaceExisting) { - List 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> buggyPostDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier>() { + @Override public List get() { + List vtable = Lists.newArrayList(); - outer: for (Method virtualMethod: methods) { - for (int i=0; i interfaces = Lists.newArrayList(getInterfaces().keySet()); + + List defaultMethods = Lists.newArrayList(); + List defaultConflictMethods = Lists.newArrayList(); + List mirandaMethods = Lists.newArrayList(); + + final HashMap 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= 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; + } + + 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()); } - continue outer; } } } + + Comparator comparator = new Comparator() { + @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> postDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier>() { + @Override public List get() { + List 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 interfaces = Lists.reverse(Lists.newArrayList(getDirectInterfaces())); + + List defaultMethods = Lists.newArrayList(); + List defaultConflictMethods = Lists.newArrayList(); + List mirandaMethods = Lists.newArrayList(); + + final HashMap 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 comparator = new Comparator() { + @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 localMethods, @Nonnull List vtable, + boolean replaceExisting, boolean sort) { + if (sort) { + ArrayList 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)) { @@ -871,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 getParameterTypes() { + return method.getParameterTypes(); + } + + @Nonnull @Override public String getReturnType() { + return method.getReturnType(); + } + + @Nonnull @Override public List getParameters() { + return method.getParameters(); + } + + @Override public int getAccessFlags() { + return method.getAccessFlags(); + } + + @Nonnull @Override public Set 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 bootClassPathDirs = Lists.newArrayList(); - String outFile = "fields.txt"; - int apiLevel = 15; - boolean experimental = false; - - for (int i=0; i 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 fields = classProto.getInstanceFields(); - String className = "Class " + classDef.getType() + " : " + fields.size() + " instance fields\n"; - outStream.write(className.getBytes()); - for (int i=0;i"); - } - - 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 bootClassPathDirs = Lists.newArrayList(); - String outFile = "vtables.txt"; - int apiLevel = 15; - boolean experimental = false; - - for (int i=0; i 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 methods = classProto.getVtable(); - String className = "Class " + classDef.getType() + " extends " + classDef.getSuperclass() + " : " + methods.size() + " methods\n"; - outStream.write(className.getBytes()); - for (int i=0;i" + 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 "); - } - - 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..0da57f16 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 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 boolean setsWideRegister() { - return false; - } - - @Override - public boolean setsRegister(int registerNumber) { - return false; - } - - @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; } @@ -1176,32 +1167,36 @@ public class MethodAnalyzer { setDestinationRegisterTypeAndPropagateChanges(analyzedInstruction, castRegisterType); } + private static boolean isNarrowingConversion(RegisterType originalType, RegisterType newType) { + if (originalType.type == null || newType.type == null) { + return false; + } + if (originalType.type.isInterface()) { + return newType.type.implementsInterface(originalType.type.getType()); + } else { + TypeProto commonSuperclass = newType.type.getCommonSuperclass(originalType.type); + return commonSuperclass.getType().equals(originalType.type.getType()); + } + } + static boolean canNarrowAfterInstanceOf(AnalyzedInstruction analyzedInstanceOfInstruction, AnalyzedInstruction analyzedIfInstruction, ClassPath classPath) { 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 isNarrowingConversion(originalType, registerType); } + } catch (UnresolvedClassException ex) { + return false; } } return false; @@ -1216,16 +1211,47 @@ public class MethodAnalyzer { 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 (analyzedInstruction.getPredecessorCount() != 1) { + return; + } + AnalyzedInstruction prevAnalyzedInstruction = analyzedInstruction.getPredecessors().first(); + if (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); + List narrowingRegisters = Lists.newArrayList(); + RegisterType newType = RegisterType.getRegisterType(classPath, (TypeReference)((Instruction22c)prevAnalyzedInstruction.instruction).getReference()); + if (instructionIndex > 1) { + // If we have something like: + // move-object/from16 v0, p1 + // instance-of v2, v0, Lblah; + // if-eqz v2, :target + // Then we need to narrow both v0 AND p1 + AnalyzedInstruction prevPrevAnalyzedInstruction = + analyzedInstructions.valueAt(instructionIndex - 2); + 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 (originalType.type != null) { + if (isNarrowingConversion(originalType, newType)) { + narrowingRegisters.add( + ((TwoRegisterInstruction)prevPrevAnalyzedInstruction.instruction).getRegisterB()); + } + } + } + } + + // Propagate the original type to the failing branch, and the new type to the successful branch + int narrowingRegister = ((Instruction22c)prevAnalyzedInstruction.instruction).getRegisterB(); + narrowingRegisters.add(narrowingRegister); + RegisterType originalType = analyzedInstruction.getPreInstructionRegisterType(narrowingRegister); + AnalyzedInstruction fallthroughInstruction = analyzedInstructions.valueAt( analyzedInstruction.getInstructionIndex() + 1); @@ -1233,16 +1259,18 @@ 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); + for (int register: narrowingRegisters) { + 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); + } } } } @@ -1695,13 +1723,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 +1761,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; - } + MethodReference replacementMethod = normalizeMethodReference(targetMethod); - 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; - } - } - - if (replacementMethod.equals(method)) { + if (replacementMethod == null || replacementMethod.equals(targetMethod)) { return true; } @@ -1839,7 +1835,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 +1858,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 +1972,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 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 primitiveMap = ImmutableBiMap.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('.', '/'); } - return javaName; + + 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('/', '.'); + } + + 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/dexbacked/DexBackedDexFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java index 32505eec..8e7127a8 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java @@ -33,9 +33,15 @@ 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.util.ExceptionWithContext; import javax.annotation.Nonnull; @@ -43,6 +49,8 @@ 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,7 +69,7 @@ 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; @@ -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,6 +104,7 @@ 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()) { @@ -148,7 +157,7 @@ public class DexBackedDexFile extends BaseDexBuffer implements DexFile { }; } - private static void verifyMagicAndByteOrder(@Nonnull byte[] buf, int offset) { + protected 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++) { @@ -265,6 +274,81 @@ public class DexBackedDexFile extends BaseDexBuffer implements DexFile { return getType(typeIndex); } + public List getStrings() { + return new AbstractList() { + @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 getTypes() { + return new AbstractList() { + @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 getMethods() { + return new AbstractList() { + @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 getFields() { + return new AbstractList() { + @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 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..dd95d282 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java @@ -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/OatFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java index dbeb67ce..95d61a1a 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 { 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; @@ -148,6 +155,18 @@ public class OatFile extends BaseDexBuffer { return UNKNOWN; } + @Nonnull + public List getBootClassPath() { + if (getOatVersion() < 75) { + return ImmutableList.of(); + } + String bcp = oatHeader.getKeyValue("bootclasspath"); + if (bcp == null) { + return ImmutableList.of(); + } + return Arrays.asList(bcp.split(":")); + } + @Nonnull public List getDexFiles() { return new AbstractForwardSequentialList() { @@ -156,44 +175,44 @@ public class OatFile extends BaseDexBuffer { } @Nonnull @Override public Iterator iterator() { - return new Iterator() { - int index = 0; - int offset = oatHeader.getDexListStart(); - - @Override public boolean hasNext() { - return index < size(); + return Iterators.transform(new DexEntryIterator(), new Function() { + @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); - } - - @Override public void remove() { - throw new UnsupportedOperationException(); - } - }; + }); } }; } - public class OatDexFile extends DexBackedDexFile { + @Nonnull @Override public List getDexEntryNames() throws IOException { + return new AbstractForwardSequentialList() { + @Override public int size() { + return oatHeader.getDexFileCount(); + } + + @Nonnull @Override public Iterator iterator() { + return Iterators.transform(new DexEntryIterator(), new Function() { + @Nullable @Override public String apply(DexEntry dexEntry) { + return dexEntry.entryName; + } + }); + } + }; + } + + @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) { @@ -201,8 +220,12 @@ public class OatFile extends BaseDexBuffer { 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 +234,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 '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 +534,64 @@ 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 int dexOffset; + + public DexEntry(String entryName, int dexOffset) { + this.entryName = entryName; + this.dexOffset = dexOffset; + } + + public OatDexFile getDexFile() { + return new OatDexFile(dexOffset, entryName); + } + } + + private class DexEntryIterator implements Iterator { + 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) + oatHeader.headerOffset; + offset += 4; + + + 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, dexOffset); + } + + @Override public void remove() { + throw new UnsupportedOperationException(); + } } public static class InvalidOatFileException extends RuntimeException { @@ -493,4 +603,5 @@ public class OatFile extends BaseDexBuffer { public static class NotAnOatFileException extends RuntimeException { public NotAnOatFileException() {} } + } \ 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..4c0f9060 --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java @@ -0,0 +1,193 @@ +/* + * 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 javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.EOFException; +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; + +import static org.jf.dexlib2.dexbacked.DexBackedDexFile.verifyMagicAndByteOrder; + +/** + * Represents a zip file that contains dex files (i.e. an apk or jar file) + */ +public class ZipDexContainer implements MultiDexContainer { + + 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; + } + + /** + * 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 getDexEntryNames() throws IOException { + List entryNames = Lists.newArrayList(); + ZipFile zipFile = getZipFile(); + try { + Enumeration 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() { + try { + getZipFile(); + return true; + } catch (IOException ex) { + return false; + } catch (NotAZipFileException ex) { + return false; + } + } + + 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; + } + } + + private boolean isDex(@Nonnull ZipFile zipFile, @Nonnull ZipEntry zipEntry) throws IOException { + InputStream inputStream = zipFile.getInputStream(zipEntry); + try { + inputStream.mark(44); + byte[] partialHeader = new byte[44]; + try { + ByteStreams.readFully(inputStream, partialHeader); + } catch (EOFException ex) { + return false; + } + + try { + verifyMagicAndByteOrder(partialHeader, 0); + } catch (NotADexFile ex) { + return false; + } + return true; + } finally { + inputStream.close(); + } + } + + private ZipFile getZipFile() throws IOException { + try { + return new ZipFile(zipFilePath); + } catch (IOException ex) { + throw new NotAZipFileException(); + } + } + + @Nonnull + private 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/iface/MultiDexContainer.java b/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java new file mode 100644 index 00000000..251ecdef --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java @@ -0,0 +1,70 @@ +/* + * 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 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 { + /** + * @return A list of the names of dex entries in this container + */ + @Nonnull List 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; + + /** + * 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 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 classes; @Nonnull private final Opcodes opcodes; - @Deprecated - public ImmutableDexFile(@Nullable Collection classes) { - this.classes = ImmutableClassDef.immutableSetOf(classes); - this.opcodes = Opcodes.forApi(19); - } - - @Deprecated - public ImmutableDexFile(@Nullable ImmutableSet classes) { - this.classes = ImmutableUtils.nullToEmptySet(classes); - this.opcodes = Opcodes.forApi(19); - } - public ImmutableDexFile(@Nonnull Opcodes opcodes, @Nullable Collection classes) { this.classes = ImmutableClassDef.immutableSetOf(classes); this.opcodes = opcodes; 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..5d03d3e8 100644 --- a/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java @@ -192,12 +192,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 +227,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()); } 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 { int getItemIndex(@Nonnull Key key); @Nonnull Collection> getItems(); + int getItemCount(); } 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 695109fa..32b6d357 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 @@ -443,4 +443,8 @@ public class BuilderClassPool implements ClassSection implements IndexSection { - @Nonnull protected final Map internedItems = Maps.newHashMap(); - +public abstract class BaseIndexPool extends BasePool implements IndexSection { @Nonnull @Override public Collection> getItems() { return internedItems.entrySet(); } 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..594a4051 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,9 +38,7 @@ import javax.annotation.Nonnull; import java.util.Collection; import java.util.Map; -public abstract class BaseOffsetPool implements OffsetSection { - @Nonnull protected final Map internedItems = Maps.newHashMap(); - +public abstract class BaseOffsetPool extends BasePool implements OffsetSection { @Nonnull @Override public Collection> 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..765896cb --- /dev/null +++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BasePool.java @@ -0,0 +1,70 @@ +/* + * 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 implements Markable { + @Nonnull protected final Map internedItems = Maps.newLinkedHashMap(); + private int markedItemCount = -1; + + 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 keys = internedItems.keySet().iterator(); + for (int i=0; i implements ClassSection>, PoolClassDef, Field, PoolMethod, Set, EncodedValue> { - @Nonnull private HashMap internedItems = Maps.newHashMap(); - @Nonnull private final StringPool stringPool; @Nonnull private final TypePool typePool; @Nonnull private final FieldPool fieldPool; 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..c0c2e314 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 @@ -56,16 +56,11 @@ public class DexPool extends DexWriter>, Field, PoolMethod, EncodedValue, AnnotationElement> { - @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[] { + (Markable)stringSection, (Markable)typeSection, (Markable)protoSection, (Markable)fieldSection, + (Markable)methodSection, (Markable)classSection, (Markable)typeListSection, (Markable)annotationSection, + (Markable)annotationSetSection + }; @Nonnull public static DexPool makeDexPool(@Nonnull Opcodes opcodes) { @@ -84,29 +79,60 @@ public class DexPool extends DexWriter { - @Nonnull protected final Map internedItems = Maps.newHashMap(); - +public abstract class StringTypeBasePool extends BasePool + implements NullableIndexSection, Markable { @Nonnull @Override public Collection> getItems() { return internedItems.entrySet(); } 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..de5b05fa --- /dev/null +++ b/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java @@ -0,0 +1,227 @@ +/* + * 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 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 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 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 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 { + @Nonnull private final Map entries; + + public TestMultiDexContainer(@Nonnull Map entries) { + this.entries = entries; + } + + @Nonnull @Override public List getDexEntryNames() throws IOException { + List entryNames = Lists.newArrayList(); + + for (Entry 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; + } + } +} 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;"), + ImmutableSet 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;"), + // 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;") - )))); + // 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 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.of(), + ImmutableList.of(), ImmutableList.of(), dexFile); + ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART); - ClassPath classPath = ClassPath.fromClassPath(ImmutableList.of(), ImmutableList.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 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.of(), + ImmutableList.of(), ImmutableList.of(), dexFile); + ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART); - ClassPath classPath = ClassPath.fromClassPath(ImmutableList.of(), ImmutableList.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 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.of(), + ImmutableList.of(), ImmutableList.of(), dexFile); + ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART); - ClassPath classPath = ClassPath.fromClassPath(ImmutableList.of(), ImmutableList.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..2588d58f --- /dev/null +++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/MethodAnalyzerTest.java @@ -0,0 +1,156 @@ +/* + * 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 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; + +public class MethodAnalyzerTest { + + @Test + public void testInstanceOfNarrowingEqz() 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.getDefault(), Collections.singletonList(classDef)); + + ClassPath classPath = new ClassPath(new DexClassProvider(dexFile)); + MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false); + + List 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 testInstanceOfNarrowingNez() 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 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 testInstanceOfNarrowingAfterMove() 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 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()); + } +} 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 classes = ImmutableSet.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 classes = ImmutableSet.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..5bc098ef --- /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.newArrayList( + new ImmutableField("Lcls1;", "field1", "I", AccessFlags.PUBLIC.getValue(), null, null) + ), + Lists.newArrayList( + new ImmutableMethod("Lcls1", "method1", + Lists.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.newArrayList( + new ImmutableField("Lcls2;", "field2", "D", AccessFlags.PUBLIC.getValue(), null, null) + ), + Lists.newArrayList( + new ImmutableMethod("Lcls2;", "method2", + Lists.newArrayList(new ImmutableMethodParameter("D", null, null)), "V", + AccessFlags.PUBLIC.getValue(), null, null)) + ); + + RawDexFile dexFile1; + { + MemoryDataStore dataStore = new MemoryDataStore(); + DexPool dexPool = DexPool.makeDexPool(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 = DexPool.makeDexPool(Opcodes.getDefault()); + dexPool.internClass(class1); + dexPool.writeTo(dataStore); + dexFile2 = new RawDexFile(Opcodes.getDefault(), dataStore.getData()); + } + + List mapItems1 = dexFile1.getMapItems(); + List mapItems2 = dexFile2.getMapItems(); + for (int i=0; i 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/smali/build.gradle b/smali/build.gradle index 318b5a97..bf362c43 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,7 @@ 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[]); }' 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 = "[|]+") + private List input; + + public AssembleCommand(@Nonnull List 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 commands; + + public HelpCommand(@Nonnull List 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 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 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.newArrayList()); + } + + public static void main(String[] args) { + Main main = new Main(); + + JCommander jc = new JCommander(main); + main.jc = jc; + jc.setProgramName("smali"); + List 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..c00da31f --- /dev/null +++ b/smali/src/main/java/org/jf/smali/Smali.java @@ -0,0 +1,206 @@ +/* + * 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 input) throws IOException { + LinkedHashSet filesToProcessSet = new LinkedHashSet(); + + 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)); + + ExecutorService executor = Executors.newFixedThreadPool(options.jobs); + List> tasks = Lists.newArrayList(); + + for (final File file: filesToProcessSet) { + tasks.add(executor.submit(new Callable() { + @Override public Boolean call() throws Exception { + return assembleSmaliFile(file, dexBuilder, options); + } + })); + } + + for (Future 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 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 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; + } +} 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..b76641b2 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 = DexBuilder.makeDexBuilder(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 100755 index 46ad1369..00000000 --- a/smali/src/main/java/org/jf/smali/main.java +++ /dev/null @@ -1,494 +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 filesToProcessSet = new LinkedHashSet(); - - 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> tasks = Lists.newArrayList(); - - for (final File file: filesToProcessSet) { - tasks.add(executor.submit(new Callable() { - @Override public Boolean call() throws Exception { - return assembleSmaliFile(file, dexBuilder, options); - } - })); - } - - for (Future 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 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 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 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; - } finally { - if (fis != null) { - fis.close(); - } - } - } - - - /** - * 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] [--] [|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" + - " (.methods by default)") - .hasOptionalArg() - .withArgName("FILE") - .create("m"); - - Option listFieldsOption = OptionBuilder.withLongOpt("list-fields") - .withDescription("Lists all the field references to FILE" + - " (.fields by default)") - .hasOptionalArg() - .withArgName("FILE") - .create("f"); - - Option listClassesOption = OptionBuilder.withLongOpt("list-types") - .withDescription("Lists all the type references to FILE" + - " (.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/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/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 + * prefix), or 0 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 + * prefix), or 0 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 basePath = getPathComponents(canonicalBaseFile); - ArrayList pathToRelativize = getPathComponents(canonicalFileToRelativize); + List basePath = getPathComponents(canonicalBaseFile); + List 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 getPathComponents(File file) { + private static List getPathComponents(File file) { ArrayList path = new ArrayList(); 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,8 +33,91 @@ 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 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() { + @Override + public Iterator iterator() { + return new Iterator() { + 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; +public class WrappedIndentingWriter extends FilterWriter { - /** > 0; the maximum output width */ - private final int width; - - /** > 0; the maximum indent */ private final int maxIndent; + private final int maxWidth; - /** >= 0; current output column (zero-based) */ - private int column; + private int currentIndent = 0; + private final StringBuilder line = new StringBuilder(); - /** 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 - * prefix), or 0 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); - - 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(); + this.maxIndent = maxIndent; + this.maxWidth = maxWidth; } - /** - * Constructs a no-prefix instance. - * - * @param out non-null; writer to send final output to - * @param width >= 0; the maximum output width (not including - * prefix), or 0 for no maximum - */ - public WrappedIndentingWriter(Writer out, int width) { - this(out, width, ""); + private void writeIndent() throws IOException { + for (int i=0; i= 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; - } + private int getIndent() { + if (currentIndent < 0) { + return 0; + } + if (currentIndent > maxIndent) { + return maxIndent; + } + return currentIndent; + } + + public void indent(int indent) { + currentIndent += indent; + } + + public void deindent(int indent) { + currentIndent -= indent; + } + + private void wrapLine() throws IOException { + List wrapped = Lists.newArrayList(StringWrapper.wrapStringOnBreaks(line.toString(), maxWidth)); + out.write(wrapped.get(0), 0, wrapped.get(0).length()); + out.write('\n'); + + line.replace(0, line.length(), ""); + writeIndent(); + for (int i=1; 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 0) { - write(str.charAt(off)); - off++; - len--; - } + @Override public void write(String str, int off, int len) throws IOException { + for (int i=0; i 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 commandAncestors; + + public Command(@Nonnull List 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 getCommandHierarchy() { + List 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 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 getSortedParameters(JCommander jc) { + List parameters = Lists.newArrayList(jc.getParameters()); + + final Pattern pattern = Pattern.compile("^-*(.*)$"); + + Collections.sort(parameters, new Comparator() { + @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 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(" []"); + } + } + + if (!leafJc.getCommands().isEmpty()) { + writer.write(" []]"); + } + + if (leafJc.getMainParameter() != null) { + String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameter()); + if (argumentNames.length == 0) { + writer.write(" "); + } 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(""); + } + } + } + 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(""); + } + + 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> entryList = Lists.newArrayList(leafJc.getCommands().entrySet()); + Collections.sort(entryList, new Comparator>() { + @Override public int compare(Entry o1, Entry o2) { + return o1.getKey().compareTo(o2.getKey()); + } + }); + + for (Entry 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 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,10 +31,34 @@ 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( @@ -115,4 +139,15 @@ public class StringWrapperTest { Assert.assertEquals(expected[i], actual[i]); } } + + public static void validateResult2(String[] expected, String textToWrap, int maxWidth) { + List result = Lists.newArrayList(StringWrapper.wrapStringOnBreaks(textToWrap, maxWidth)); + + Assert.assertEquals(expected.length, result.size()); + int i; + for (i=0; i