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 extends AnnotationElement> 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 extends EncodedValue> 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