From b6a1ae3481931d70f7bb56f4b4e805a35a3a263e Mon Sep 17 00:00:00 2001 From: Ben Gruver Date: Fri, 26 Feb 2021 11:21:33 -0800 Subject: [PATCH] Add new BaksmaliWriter/BaksmaliFormatter classes These are intended to be the centralized place for most individual items to be formatted and converted to/written as text --- .../baksmali/formatter/BaksmaliFormatter.java | 55 +++ .../jf/baksmali/formatter/BaksmaliWriter.java | 368 ++++++++++++++++++ .../formatter/BaksmaliWriterTest.java | 322 +++++++++++++++ .../formatter/BaksmaliWriterTypeTest.java | 134 +++++++ .../main/java/org/jf/util/StringUtils.java | 5 + 5 files changed, 884 insertions(+) create mode 100644 baksmali/src/main/java/org/jf/baksmali/formatter/BaksmaliFormatter.java create mode 100644 baksmali/src/main/java/org/jf/baksmali/formatter/BaksmaliWriter.java create mode 100644 baksmali/src/test/java/org/jf/baksmali/formatter/BaksmaliWriterTest.java create mode 100644 baksmali/src/test/java/org/jf/baksmali/formatter/BaksmaliWriterTypeTest.java diff --git a/baksmali/src/main/java/org/jf/baksmali/formatter/BaksmaliFormatter.java b/baksmali/src/main/java/org/jf/baksmali/formatter/BaksmaliFormatter.java new file mode 100644 index 00000000..5787a763 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/formatter/BaksmaliFormatter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021, 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.formatter; + +import org.jf.dexlib2.formatter.DexFormatter; + +import javax.annotation.Nullable; +import java.io.Writer; + +public class BaksmaliFormatter extends DexFormatter { + + @Nullable private final String classContext; + + public BaksmaliFormatter() { + this(null); + } + + public BaksmaliFormatter(@Nullable String classContext) { + this.classContext = classContext; + } + + @Override + public BaksmaliWriter getWriter(Writer writer) { + return new BaksmaliWriter(writer, classContext); + } +} diff --git a/baksmali/src/main/java/org/jf/baksmali/formatter/BaksmaliWriter.java b/baksmali/src/main/java/org/jf/baksmali/formatter/BaksmaliWriter.java new file mode 100644 index 00000000..d98a1dd8 --- /dev/null +++ b/baksmali/src/main/java/org/jf/baksmali/formatter/BaksmaliWriter.java @@ -0,0 +1,368 @@ +/* + * Copyright 2021, 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.formatter; + +import org.jf.dexlib2.MethodHandleType; +import org.jf.dexlib2.ValueType; +import org.jf.dexlib2.formatter.DexFormattedWriter; +import org.jf.dexlib2.iface.AnnotationElement; +import org.jf.dexlib2.iface.reference.CallSiteReference; +import org.jf.dexlib2.iface.reference.FieldReference; +import org.jf.dexlib2.iface.reference.MethodHandleReference; +import org.jf.dexlib2.iface.reference.MethodReference; +import org.jf.dexlib2.iface.value.*; +import org.jf.util.IndentingWriter; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.Writer; +import java.util.Collection; + + +/** + * A specialized version of DexFormattedWriter that handles quoting + * simple names containing spaces. + */ +public class BaksmaliWriter extends DexFormattedWriter { + + @Nullable private final String classContext; + + protected final char[] buffer = new char[24]; + + public BaksmaliWriter(Writer writer) { + this(writer, null); + } + + /** + * Constructs a new BaksmaliWriter + * + * @param writer The {@link IndentingWriter} to write to + * @param classContext If provided, the class will be elided from any field/method descriptors whose containing + * class match this instance's classContext. + */ + public BaksmaliWriter(Writer writer, @Nullable String classContext) { + super(writer instanceof IndentingWriter ? writer : new IndentingWriter(writer)); + this.classContext = classContext; + } + + @Override public void writeMethodDescriptor(MethodReference methodReference) throws IOException { + if (methodReference.getDefiningClass().equals(classContext)) { + writeShortMethodDescriptor(methodReference); + } else { + super.writeMethodDescriptor(methodReference); + } + } + + @Override public void writeFieldDescriptor(FieldReference fieldReference) throws IOException { + if (fieldReference.getDefiningClass().equals(classContext)) { + writeShortFieldDescriptor(fieldReference); + } else { + super.writeFieldDescriptor(fieldReference); + } + } + + @Override + protected void writeClass(CharSequence type) throws IOException { + assert type.charAt(0) == 'L'; + + writer.write(type.charAt(0)); + + int startIndex = 1; + boolean hasSpace = false; + int i; + for (i = startIndex; i < type.length(); i++) { + char c = type.charAt(i); + + if (Character.getType(c) == Character.SPACE_SEPARATOR) { + hasSpace = true; + } else if (c == '/') { + if (i == startIndex) { + throw new IllegalArgumentException( + String.format("Invalid type string: %s", type)); + } + + writeSimpleName(type.subSequence(startIndex, i), hasSpace); + writer.write(type.charAt(i)); + hasSpace = false; + startIndex = i+1; + } else if (c == ';') { + if (i == startIndex) { + throw new IllegalArgumentException( + String.format("Invalid type string: %s", type)); + } + + writeSimpleName(type.subSequence(startIndex, i), hasSpace); + writer.write(type.charAt(i)); + break; + } + } + + if (i != type.length() - 1 || type.charAt(i) != ';') { + throw new IllegalArgumentException( + String.format("Invalid type string: %s", type)); + } + } + + @Override + public void writeSimpleName(CharSequence simpleName) throws IOException { + boolean hasSpace = false; + for (int i = 0; i < simpleName.length(); i++) { + if (Character.getType(simpleName.charAt(i)) == Character.SPACE_SEPARATOR) { + hasSpace = true; + break; + } + } + writeSimpleName(simpleName, hasSpace); + } + + /** + * Writes the given simple name, potentially quoting it if requested. + * + *

The simple name will be quoted with backticks if quoted is true + * + *

A simple name should typically be quoted if it is meant to be human readable, and it contains spaces. + * + * @param simpleName The simple name to write. See: https://source.android.com/devices/tech/dalvik/dex-format#simplename + */ + public void writeSimpleName(CharSequence simpleName, boolean quoted) throws IOException { + if (quoted) { + writer.write('`'); + } + writer.append(simpleName); + if (quoted) { + writer.write('`'); + } + } + + public void writeEncodedValue(EncodedValue encodedValue) throws IOException { + switch (encodedValue.getValueType()) { + case ValueType.BOOLEAN: + writeBooleanEncodedValue((BooleanEncodedValue) encodedValue); + break; + case ValueType.BYTE: + writeIntegralValue(((ByteEncodedValue) encodedValue).getValue(), 't'); + break; + case ValueType.CHAR: + writeCharEncodedValue((CharEncodedValue) encodedValue); + break; + case ValueType.SHORT: + writeIntegralValue(((ShortEncodedValue) encodedValue).getValue(), 's'); + break; + case ValueType.INT: + writeIntegralValue(((IntEncodedValue) encodedValue).getValue(), null); + break; + case ValueType.LONG: + writeIntegralValue(((LongEncodedValue)encodedValue).getValue(), 'L'); + break; + case ValueType.FLOAT: + writeFloatEncodedValue((FloatEncodedValue) encodedValue); + break; + case ValueType.DOUBLE: + writeDoubleEncodedValue((DoubleEncodedValue) encodedValue); + break; + case ValueType.ANNOTATION: + writeAnnotation((AnnotationEncodedValue)encodedValue); + break; + case ValueType.ARRAY: + writeArray((ArrayEncodedValue)encodedValue); + break; + case ValueType.STRING: + writeQuotedString(((StringEncodedValue)encodedValue).getValue()); + break; + case ValueType.FIELD: + writeFieldDescriptor(((FieldEncodedValue)encodedValue).getValue()); + break; + case ValueType.ENUM: + writeEnum((EnumEncodedValue) encodedValue); + break; + case ValueType.METHOD: + writeMethodDescriptor(((MethodEncodedValue)encodedValue).getValue()); + break; + case ValueType.TYPE: + writeType(((TypeEncodedValue)encodedValue).getValue()); + break; + case ValueType.METHOD_TYPE: + writeMethodProtoDescriptor(((MethodTypeEncodedValue)encodedValue).getValue()); + break; + case ValueType.METHOD_HANDLE: + writeMethodHandle(((MethodHandleEncodedValue)encodedValue).getValue()); + break; + case ValueType.NULL: + writer.write("null"); + break; + default: + throw new IllegalArgumentException("Unknown encoded value type"); + } + } + + protected void writeBooleanEncodedValue(BooleanEncodedValue encodedValue) throws IOException { + writer.write(Boolean.toString(encodedValue.getValue())); + } + + protected void writeIntegralValue(long value, @Nullable Character suffix) throws IOException { + if (value < 0) { + writer.write("-0x"); + indentingWriter().printUnsignedLongAsHex(-value); + } else { + writer.write("0x"); + indentingWriter().printUnsignedLongAsHex(value); + } + if (suffix != null) { + writer.write(suffix); + } + } + + protected void writeCharEncodedValue(CharEncodedValue encodedValue) throws IOException { + writer.write('\''); + + char c = encodedValue.getValue(); + if ((c >= ' ') && (c < 0x7f)) { + if ((c == '\'') || (c == '\"') || (c == '\\')) { + writer.write('\\'); + } + writer.write(c); + return; + } else if (c <= 0x7f) { + switch (c) { + case '\n': writer.write("\\n"); return; + case '\r': writer.write("\\r"); return; + case '\t': writer.write("\\t"); return; + } + } + + writer.write("\\u"); + writer.write(Character.forDigit(c >> 12, 16)); + writer.write(Character.forDigit((c >> 8) & 0x0f, 16)); + writer.write(Character.forDigit((c >> 4) & 0x0f, 16)); + writer.write(Character.forDigit(c & 0x0f, 16)); + + writer.write('\''); + } + + protected void writeFloatEncodedValue(FloatEncodedValue encodedValue) throws IOException { + writer.write(Float.toString(encodedValue.getValue())); + writer.write('f'); + } + + protected void writeDoubleEncodedValue(DoubleEncodedValue encodedValue) throws IOException { + writer.write(Double.toString(encodedValue.getValue())); + } + + protected void writeEnum(EnumEncodedValue encodedValue) throws IOException { + writer.write(".enum "); + writeFieldDescriptor(encodedValue.getValue()); + } + + /** + * Write the given {@link AnnotationEncodedValue}. + */ + protected void writeAnnotation(AnnotationEncodedValue annotation) throws IOException { + writer.write(".subannotation "); + writeType(annotation.getType()); + writer.write('\n'); + + writeAnnotationElements(annotation.getElements()); + + writer.write(".end subannotation"); + } + + public void writeAnnotationElements( + @Nonnull Collection annotationElements) throws IOException { + indent(4); + for (AnnotationElement annotationElement: annotationElements) { + writeSimpleName(annotationElement.getName()); + writer.write(" = "); + writeEncodedValue(annotationElement.getValue()); + writer.write('\n'); + } + deindent(4); + } + + /** + * Write the given {@link ArrayEncodedValue}. + */ + protected void writeArray(ArrayEncodedValue array) throws IOException { + writer.write('{'); + Collection values = array.getValue(); + if (values.size() == 0) { + writer.write('}'); + return; + } + + writer.write('\n'); + indent(4); + boolean first = true; + for (EncodedValue encodedValue: values) { + if (!first) { + writer.write(",\n"); + } + first = false; + + writeEncodedValue(encodedValue); + } + deindent(4); + writer.write("\n}"); + } + + @Override public void writeCallSite(CallSiteReference callSiteReference) throws IOException { + writeSimpleName(callSiteReference.getName()); + writer.write('('); + writeQuotedString(callSiteReference.getMethodName()); + writer.write(", "); + writeMethodProtoDescriptor(callSiteReference.getMethodProto()); + + for (EncodedValue encodedValue : callSiteReference.getExtraArguments()) { + writer.write(", "); + writeEncodedValue(encodedValue); + } + + writer.write(")@"); + MethodHandleReference methodHandle = callSiteReference.getMethodHandle(); + if (methodHandle.getMethodHandleType() != MethodHandleType.INVOKE_STATIC) { + throw new IllegalArgumentException("The linker method handle for a call site must be of type invoke-static"); + } + writeMethodDescriptor((MethodReference) callSiteReference.getMethodHandle().getMemberReference()); + } + + public IndentingWriter indentingWriter() { + return (IndentingWriter) writer; + } + + public void indent(int indentAmount) { + ((IndentingWriter) writer).indent(indentAmount); + } + + public void deindent(int indentAmount) { + ((IndentingWriter) writer).deindent(indentAmount); + } +} diff --git a/baksmali/src/test/java/org/jf/baksmali/formatter/BaksmaliWriterTest.java b/baksmali/src/test/java/org/jf/baksmali/formatter/BaksmaliWriterTest.java new file mode 100644 index 00000000..be5d6206 --- /dev/null +++ b/baksmali/src/test/java/org/jf/baksmali/formatter/BaksmaliWriterTest.java @@ -0,0 +1,322 @@ +/* + * Copyright 2021, 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.formatter; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.jf.dexlib2.MethodHandleType; +import org.jf.dexlib2.iface.reference.MethodHandleReference; +import org.jf.dexlib2.immutable.ImmutableAnnotationElement; +import org.jf.dexlib2.immutable.reference.*; +import org.jf.dexlib2.immutable.value.*; +import org.jf.util.IndentingWriter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.io.StringWriter; + +public class BaksmaliWriterTest { + + private StringWriter stringWriter; + private IndentingWriter output; + + @Before + public void setup() { + stringWriter = new StringWriter(); + output = new IndentingWriter(stringWriter); + } + + @Test + public void testWriteMethodDescriptor_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeMethodDescriptor(getMethodReferenceWithSpaces()); + + Assert.assertEquals( + "Ldefining/class/`with spaces`;->`methodName with spaces`(L`param with spaces 1`;L`param with spaces 2`;)" + + "Lreturn/type/`with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteShortMethodDescriptor_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeShortMethodDescriptor(getMethodReferenceWithSpaces()); + + Assert.assertEquals( + "`methodName with spaces`(L`param with spaces 1`;L`param with spaces 2`;)" + + "Lreturn/type/`with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteMethodProtoDescriptor_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeMethodProtoDescriptor(getMethodProtoReferenceWithSpaces()); + + Assert.assertEquals( + "(L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteFieldDescriptor_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeFieldDescriptor(getFieldReferenceWithSpaces()); + + Assert.assertEquals("Ldefining/class/`with spaces`;->`fieldName with spaces`:Lfield/`type with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteShortFieldDescriptor_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeShortFieldDescriptor(getFieldReferenceWithSpaces()); + + Assert.assertEquals("`fieldName with spaces`:Lfield/`type with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteMethodHandle_fieldAccess_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeMethodHandle(getMethodHandleReferenceForFieldWithSpaces()); + + Assert.assertEquals("instance-get@Ldefining/class/`with spaces`;->`fieldName with spaces`:" + + "Lfield/`type with spaces`;", stringWriter.toString()); + } + + @Test + public void testWriteMethodHandle_methodAccess_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeMethodHandle(getMethodHandleReferenceForMethodWithSpaces()); + + Assert.assertEquals("invoke-instance@Ldefining/class/`with spaces`;->`methodName with spaces`(" + + "L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteCallsite_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeCallSite(new ImmutableCallSiteReference( + "callsiteName with spaces", + getInvokeStaticMethodHandleReferenceForMethodWithSpaces(), + "callSiteMethodName with spaces", + getMethodProtoReferenceWithSpaces(), + ImmutableList.of( + new ImmutableFieldEncodedValue(getFieldReferenceWithSpaces()), + new ImmutableMethodEncodedValue(getMethodReferenceWithSpaces())))); + + Assert.assertEquals( + "`callsiteName with spaces`(\"callSiteMethodName with spaces\", " + + "(L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;, " + + "Ldefining/class/`with spaces`;->`fieldName with spaces`:Lfield/`type with spaces`;, " + + "Ldefining/class/`with spaces`;->`methodName with spaces`(" + + "L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;)@" + + "Ldefining/class/`with spaces`;->`methodName with spaces`(" + + "L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteEncodedValue_annotation_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeEncodedValue(new ImmutableAnnotationEncodedValue( + "Lannotation/type with spaces;", + ImmutableSet.of( + new ImmutableAnnotationElement("element with spaces 1", + new ImmutableFieldEncodedValue(getFieldReferenceWithSpaces())), + new ImmutableAnnotationElement("element with spaces 2", + new ImmutableMethodEncodedValue(getMethodReferenceWithSpaces())) + ))); + + Assert.assertEquals( + ".subannotation Lannotation/`type with spaces`;\n" + + " `element with spaces 1` = Ldefining/class/`with spaces`;->`fieldName with spaces`:Lfield/`type with spaces`;\n" + + " `element with spaces 2` = Ldefining/class/`with spaces`;->`methodName with spaces`(" + + "L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;\n" + + ".end subannotation", + stringWriter.toString()); + } + + @Test + public void testWriteEncodedValue_array_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeEncodedValue(new ImmutableArrayEncodedValue(ImmutableList.of( + new ImmutableFieldEncodedValue(getFieldReferenceWithSpaces()), + new ImmutableMethodEncodedValue(getMethodReferenceWithSpaces())))); + + Assert.assertEquals( + "{\n" + + " Ldefining/class/`with spaces`;->`fieldName with spaces`:Lfield/`type with spaces`;,\n" + + " Ldefining/class/`with spaces`;->`methodName with spaces`(L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;\n" + + "}", + stringWriter.toString()); + } + + @Test + public void testWriteEncodedValue_field_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeEncodedValue(new ImmutableFieldEncodedValue(getFieldReferenceWithSpaces())); + + Assert.assertEquals( + "Ldefining/class/`with spaces`;->`fieldName with spaces`:Lfield/`type with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteEncodedValue_enum_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeEncodedValue(new ImmutableEnumEncodedValue(getFieldReferenceWithSpaces())); + + Assert.assertEquals( + ".enum Ldefining/class/`with spaces`;->`fieldName with spaces`:Lfield/`type with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteEncodedValue_method_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeEncodedValue(new ImmutableMethodEncodedValue(getMethodReferenceWithSpaces())); + + Assert.assertEquals( + "Ldefining/class/`with spaces`;->`methodName with spaces`(" + + "L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteEncodedValue_type_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeEncodedValue(new ImmutableTypeEncodedValue("Ltest/type with spaces;")); + + Assert.assertEquals( + "Ltest/`type with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteEncodedValue_methodType_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeEncodedValue(new ImmutableMethodTypeEncodedValue(getMethodProtoReferenceWithSpaces())); + + Assert.assertEquals( + "(L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;", + stringWriter.toString()); + } + + @Test + public void testWriteEncodedValue_methodHandle_withSpaces() throws IOException { + BaksmaliWriter writer = + new BaksmaliWriter(output); + + writer.writeEncodedValue( + new ImmutableMethodHandleEncodedValue(getMethodHandleReferenceForMethodWithSpaces())); + + Assert.assertEquals( + "invoke-instance@Ldefining/class/`with spaces`;->`methodName with spaces`(" + + "L`param with spaces 1`;L`param with spaces 2`;)Lreturn/type/`with spaces`;", + stringWriter.toString()); + } + + private ImmutableMethodReference getMethodReferenceWithSpaces() { + return new ImmutableMethodReference( + "Ldefining/class/with spaces;", + "methodName with spaces", + ImmutableList.of("Lparam with spaces 1;", "Lparam with spaces 2;"), + "Lreturn/type/with spaces;"); + } + + private ImmutableMethodProtoReference getMethodProtoReferenceWithSpaces() { + return new ImmutableMethodProtoReference( + ImmutableList.of("Lparam with spaces 1;", "Lparam with spaces 2;"), + "Lreturn/type/with spaces;"); + } + + private ImmutableFieldReference getFieldReferenceWithSpaces() { + return new ImmutableFieldReference( + "Ldefining/class/with spaces;", + "fieldName with spaces", + "Lfield/type with spaces;"); + } + + private MethodHandleReference getMethodHandleReferenceForFieldWithSpaces() { + return new ImmutableMethodHandleReference( + MethodHandleType.INSTANCE_GET, + getFieldReferenceWithSpaces()); + } + + + private ImmutableMethodHandleReference getMethodHandleReferenceForMethodWithSpaces() { + return new ImmutableMethodHandleReference( + MethodHandleType.INVOKE_INSTANCE, + getMethodReferenceWithSpaces()); + } + + private MethodHandleReference getInvokeStaticMethodHandleReferenceForMethodWithSpaces() { + return new ImmutableMethodHandleReference( + MethodHandleType.INVOKE_STATIC, + getMethodReferenceWithSpaces()); + } +} diff --git a/baksmali/src/test/java/org/jf/baksmali/formatter/BaksmaliWriterTypeTest.java b/baksmali/src/test/java/org/jf/baksmali/formatter/BaksmaliWriterTypeTest.java new file mode 100644 index 00000000..d725bcc9 --- /dev/null +++ b/baksmali/src/test/java/org/jf/baksmali/formatter/BaksmaliWriterTypeTest.java @@ -0,0 +1,134 @@ +/* + * Copyright 2021, 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.formatter; + +import com.google.common.collect.Lists; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.List; + +public class BaksmaliWriterTypeTest { + + @Test + public void testWriteType_unquoted() throws IOException { + String[] typeStrings = new String[] { + "Ljava/lang/Object;", + "Z", + "B", + "S", + "C", + "I", + "J", + "F", + "D", + "V", + "[D", + "[[D", + "[Ljava/lang/Object;", + "[[Ljava/lang/Object;", + "LC;" + }; + + for (String typeString: typeStrings) { + Assert.assertEquals(typeString, performWriteType(typeString)); + } + } + + @Test + public void testWriteType_withSpaces() throws IOException { + Assert.assertEquals("Lmy/`pack age`/`class name`;", + performWriteType("Lmy/pack age/class name;")); + + Assert.assertEquals("L` `;", performWriteType("L ;")); + Assert.assertEquals("Lmy/` `/class;", performWriteType("Lmy/ /class;")); + + Assert.assertEquals("L` `;", performWriteType("L ;")); + + Assert.assertEquals("L` ab`;", performWriteType("L ab;")); + Assert.assertEquals("L`ab `;", performWriteType("Lab ;")); + + List spaceCharacters = Lists.newArrayList( + '\u0020', + '\u00A0', + '\u1680', + '\u202f', + '\u205f', + '\u3000'); + for (char c = 0x2000; c <= 0x200a; c++) { + spaceCharacters.add(c); + } + + for (char c: spaceCharacters) { + Assert.assertEquals( + String.format("Error while testing character \\u%04x", (int)c), + String.format("Lmy/`%c`/package;", c), + performWriteType(String.format("Lmy/%c/package;", c))); + } + } + + @Test + public void testWriteType_invalid() throws IOException { + + assertWriteTypeFails("L;"); + assertWriteTypeFails("H"); + assertWriteTypeFails("L/blah;"); + assertWriteTypeFails("La//b;"); + + assertWriteTypeFails("La//b"); + assertWriteTypeFails("La//b "); + + assertWriteTypeFails("["); + + assertWriteTypeFails("[L"); + + assertWriteTypeFails("[L "); + } + + private void assertWriteTypeFails(String input) throws IOException { + try { + performWriteType(input); + Assert.fail("Expected failure did not occur"); + } catch (IllegalArgumentException ex) { + // expected exception + } + } + + private String performWriteType(String input) throws IOException { + StringWriter stringWriter = new StringWriter(); + BaksmaliWriter writer = new BaksmaliWriter(stringWriter, null); + writer.writeType(input); + return stringWriter.toString(); + } +} diff --git a/dexlib2/src/main/java/org/jf/util/StringUtils.java b/dexlib2/src/main/java/org/jf/util/StringUtils.java index 01fcb8f9..ba61b9da 100644 --- a/dexlib2/src/main/java/org/jf/util/StringUtils.java +++ b/dexlib2/src/main/java/org/jf/util/StringUtils.java @@ -35,6 +35,11 @@ import java.io.IOException; import java.io.Writer; public class StringUtils { + + /** + * @deprecated Use {@link org.jf.baksmali.formatter.BaksmaliWriter#writeCharEncodedValue} + */ + @Deprecated public static void writeEscapedChar(Writer writer, char c) throws IOException { if ((c >= ' ') && (c < 0x7f)) { if ((c == '\'') || (c == '\"') || (c == '\\')) {