Add tests of DEX with identifiers with white space characters
Bug: b/269089718
Change-Id: If8a9e928c0caea3e7f718cd535f1b58521829607
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 6d43e68..5d8832f 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -795,6 +795,12 @@
private byte[] dexVersionBytes() {
if (options.testing.dexContainerExperiment) {
+ return DexVersion.V41.getBytes();
+ }
+ // TODO(b/269089718): Remove this testing option and always emit DEX version 040 if DEX contains
+ // identifiers with whitespace.
+ if (options.testing.dexVersion40FromApiLevel30
+ && options.getMinApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.R)) {
return DexVersion.V40.getBytes();
}
return options.testing.forceDexVersionBytes != null
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
index 5be322e..2e7e2e3 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
@@ -99,6 +99,8 @@
case V39:
return AndroidApiLevel.P;
case V40:
+ return AndroidApiLevel.R;
+ case V41:
return AndroidApiLevel.ANDROID_PLATFORM;
default:
throw new Unreachable();
diff --git a/src/main/java/com/android/tools/r8/utils/DexVersion.java b/src/main/java/com/android/tools/r8/utils/DexVersion.java
index d094cf7..e1b4588 100644
--- a/src/main/java/com/android/tools/r8/utils/DexVersion.java
+++ b/src/main/java/com/android/tools/r8/utils/DexVersion.java
@@ -13,7 +13,8 @@
V37(37, new byte[] {'0', '3', '7'}),
V38(38, new byte[] {'0', '3', '8'}),
V39(39, new byte[] {'0', '3', '9'}),
- V40(40, new byte[] {'0', '4', '0'});
+ V40(40, new byte[] {'0', '4', '0'}),
+ V41(41, new byte[] {'0', '4', '1'});
private final int dexVersion;
@@ -47,6 +48,8 @@
case Sv2:
case S:
case R:
+ // Dex version should have been V40 starting from API level 30, see b/269089718.
+ // return DexVersion.V40;
case Q:
case P:
return DexVersion.V39;
@@ -97,6 +100,8 @@
return Optional.of(V39);
case 40:
return Optional.of(V40);
+ case 41:
+ return Optional.of(V41);
default:
return Optional.empty();
}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index e7dfeee..ce1acd6 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -154,7 +154,7 @@
public static final int SUPPORTED_DEX_VERSION =
AndroidApiLevel.LATEST.getDexVersion().getIntValue();
- public static final int EXPERIMENTAL_DEX_VERSION = DexVersion.V40.getIntValue();
+ public static final int EXPERIMENTAL_DEX_VERSION = DexVersion.V41.getIntValue();
public static final int ASM_VERSION = Opcodes.ASM9;
@@ -1978,6 +1978,8 @@
private boolean hasReadCheckDeterminism = false;
private DeterminismChecker determinismChecker = null;
public boolean usePcEncodingInCfForTesting = false;
+ public boolean dexVersion40FromApiLevel30 =
+ System.getProperty("com.android.tools.r8.dexVersion40ForApiLevel30") != null;
public boolean dexContainerExperiment =
System.getProperty("com.android.tools.r8.dexContainerExperiment") != null;
diff --git a/src/test/java/com/android/tools/r8/dex/container/DexContainerFormatBasicTest.java b/src/test/java/com/android/tools/r8/dex/container/DexContainerFormatBasicTest.java
index d1f909d..72f856f 100644
--- a/src/test/java/com/android/tools/r8/dex/container/DexContainerFormatBasicTest.java
+++ b/src/test/java/com/android/tools/r8/dex/container/DexContainerFormatBasicTest.java
@@ -6,12 +6,14 @@
import static com.android.tools.r8.dex.Constants.CHECKSUM_OFFSET;
import static com.android.tools.r8.dex.Constants.DATA_OFF_OFFSET;
import static com.android.tools.r8.dex.Constants.DATA_SIZE_OFFSET;
+import static com.android.tools.r8.dex.Constants.DEX_MAGIC_SIZE;
import static com.android.tools.r8.dex.Constants.FILE_SIZE_OFFSET;
import static com.android.tools.r8.dex.Constants.MAP_OFF_OFFSET;
import static com.android.tools.r8.dex.Constants.SIGNATURE_OFFSET;
import static com.android.tools.r8.dex.Constants.STRING_IDS_OFF_OFFSET;
import static com.android.tools.r8.dex.Constants.STRING_IDS_SIZE_OFFSET;
import static com.android.tools.r8.dex.Constants.TYPE_STRING_ID_ITEM;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -27,6 +29,7 @@
import com.android.tools.r8.transformers.ClassTransformer;
import com.android.tools.r8.utils.AndroidApiLevel;
import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.DexVersion;
import com.android.tools.r8.utils.ZipUtils;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
@@ -80,7 +83,7 @@
.setMinApi(AndroidApiLevel.L)
.compile()
.writeToZip();
- validateDex(outputA, 2);
+ validateDex(outputA, 2, AndroidApiLevel.L.getDexVersion());
Path outputB =
testForD8(Backend.DEX)
@@ -88,7 +91,7 @@
.setMinApi(AndroidApiLevel.L)
.compile()
.writeToZip();
- validateDex(outputB, 2);
+ validateDex(outputB, 2, AndroidApiLevel.L.getDexVersion());
Path outputMerged =
testForD8(Backend.DEX)
@@ -96,7 +99,7 @@
.setMinApi(AndroidApiLevel.L)
.compile()
.writeToZip();
- validateDex(outputMerged, 4);
+ validateDex(outputMerged, 4, AndroidApiLevel.L.getDexVersion());
}
@Test
@@ -135,21 +138,22 @@
validateSingleContainerDex(outputB);
}
- private void validateDex(Path output, int expectedDexes) throws Exception {
+ private void validateDex(Path output, int expectedDexes, DexVersion expectedVersion)
+ throws Exception {
List<byte[]> dexes = unzipContent(output);
assertEquals(expectedDexes, dexes.size());
for (byte[] dex : dexes) {
- validate(dex);
+ validate(dex, expectedVersion);
}
}
private void validateSingleContainerDex(Path output) throws Exception {
List<byte[]> dexes = unzipContent(output);
assertEquals(1, dexes.size());
- validate(dexes.get(0));
+ validate(dexes.get(0), DexVersion.V41);
}
- private void validate(byte[] dex) throws Exception {
+ private void validate(byte[] dex, DexVersion expectedVersion) throws Exception {
CompatByteBuffer buffer = CompatByteBuffer.wrap(dex);
setByteOrder(buffer);
@@ -163,21 +167,47 @@
}
assertEquals(buffer.capacity(), offset);
- int lastOffset = sections.getInt(sections.size() - 1);
- int stringIdsSize = buffer.getInt(lastOffset + STRING_IDS_SIZE_OFFSET);
- int stringIdsOffset = buffer.getInt(lastOffset + STRING_IDS_OFF_OFFSET);
-
for (Integer sectionOffset : sections) {
- assertEquals(stringIdsSize, buffer.getInt(sectionOffset + STRING_IDS_SIZE_OFFSET));
- assertEquals(stringIdsOffset, buffer.getInt(sectionOffset + STRING_IDS_OFF_OFFSET));
- assertEquals(stringIdsSize, getSizeFromMap(TYPE_STRING_ID_ITEM, buffer, sectionOffset));
- assertEquals(stringIdsOffset, getOffsetFromMap(TYPE_STRING_ID_ITEM, buffer, sectionOffset));
+ validateHeader(sections, buffer, sectionOffset, expectedVersion);
validateMap(buffer, sectionOffset);
validateSignature(buffer, sectionOffset);
validateChecksum(buffer, sectionOffset);
}
}
+ private byte[] magicBytes(DexVersion version) {
+ byte[] magic = new byte[DEX_MAGIC_SIZE];
+ System.arraycopy(
+ Constants.DEX_FILE_MAGIC_PREFIX, 0, magic, 0, Constants.DEX_FILE_MAGIC_PREFIX.length);
+ System.arraycopy(
+ version.getBytes(),
+ 0,
+ magic,
+ Constants.DEX_FILE_MAGIC_PREFIX.length,
+ version.getBytes().length);
+ magic[Constants.DEX_FILE_MAGIC_PREFIX.length + version.getBytes().length] =
+ Constants.DEX_FILE_MAGIC_SUFFIX;
+ assertEquals(
+ DEX_MAGIC_SIZE, Constants.DEX_FILE_MAGIC_PREFIX.length + version.getBytes().length + 1);
+ return magic;
+ }
+
+ private void validateHeader(
+ IntList sections, CompatByteBuffer buffer, int offset, DexVersion expectedVersion) {
+ int lastOffset = sections.getInt(sections.size() - 1);
+ int stringIdsSize = buffer.getInt(lastOffset + STRING_IDS_SIZE_OFFSET);
+ int stringIdsOffset = buffer.getInt(lastOffset + STRING_IDS_OFF_OFFSET);
+
+ byte[] magic = new byte[DEX_MAGIC_SIZE];
+ buffer.get(magic);
+ assertArrayEquals(magicBytes(expectedVersion), magic);
+
+ assertEquals(stringIdsSize, buffer.getInt(offset + STRING_IDS_SIZE_OFFSET));
+ assertEquals(stringIdsOffset, buffer.getInt(offset + STRING_IDS_OFF_OFFSET));
+ assertEquals(stringIdsSize, getSizeFromMap(TYPE_STRING_ID_ITEM, buffer, offset));
+ assertEquals(stringIdsOffset, getOffsetFromMap(TYPE_STRING_ID_ITEM, buffer, offset));
+ }
+
private void validateMap(CompatByteBuffer buffer, int offset) {
int mapOffset = buffer.getInt(offset + MAP_OFF_OFFSET);
buffer.position(mapOffset);
diff --git a/src/test/java/com/android/tools/r8/dex/whitespaceinidentifiers/WhiteSpaceInIdentifiersTest.java b/src/test/java/com/android/tools/r8/dex/whitespaceinidentifiers/WhiteSpaceInIdentifiersTest.java
new file mode 100644
index 0000000..9092ea4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dex/whitespaceinidentifiers/WhiteSpaceInIdentifiersTest.java
@@ -0,0 +1,263 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.dex.whitespaceinidentifiers;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticException;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.naming.ProguardMapReader.ParseException;
+import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.DexVersion;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class WhiteSpaceInIdentifiersTest extends TestBase {
+
+ @Parameter() public TestParameters parameters;
+
+ @Parameters(name = "{0}")
+ public static List<Object[]> data() {
+ return buildParameters(getTestParameters().withDexRuntimesAndAllApiLevels().build());
+ }
+
+ private static final String EXPECTED_OUTPUT =
+ StringUtils.lines(
+ "0x20", "0xa0", "0x1680", "0x2000", "0x2001", "0x2002", "0x2003", "0x2004", "0x2005",
+ "0x2006", "0x2007", "0x2008", "0x2009", "0x200a", "0x202f", "0x205f", "0x3000");
+
+ public void test(TestCompilerBuilder<?, ?, ?, ?, ?> testBuilder) throws Exception {
+ testBuilder
+ .addProgramClassFileData(getTransformed())
+ .setMinApi(parameters.getApiLevel())
+ .applyIf(
+ parameters.getApiLevel().isLessThan(AndroidApiLevel.R),
+ b -> {
+ try {
+ b.compileWithExpectedDiagnostics(
+ diagnostics ->
+ diagnostics.assertErrorsMatch(
+ diagnosticMessage(
+ containsString("are not allowed prior to DEX version 040"))));
+ fail("Unexpected success");
+ } catch (CompilationFailedException e) {
+ // Expected.
+ }
+ },
+ b ->
+ b.run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT));
+ }
+
+ @Test
+ public void testD8() throws Exception {
+ test(testForD8(parameters.getBackend()));
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ try {
+ test(testForR8(parameters.getBackend()).addKeepMainRule(TestClass.class));
+ } catch (RuntimeException e) {
+ // TODO(b/141287396): Proguard maps with spaces in identifiers are not supported. Running
+ // on R8 always creates an inspector to find the name of the potentially renamed main class.
+ assertTrue(e.getCause() instanceof ExecutionException);
+ assertTrue(e.getCause().getCause() instanceof ParseException);
+ }
+ }
+
+ @Test
+ public void testD8MergeWithSpaces() throws Exception {
+ assumeTrue(parameters.getApiLevel().getDexVersion().isGreaterThanOrEqualTo(DexVersion.V39));
+
+ // Compile with API level 30 to allow white space in identifiers. This generates DEX
+ // version 039.
+ Path dex =
+ testForD8(parameters.getBackend())
+ .addProgramClassFileData(getTransformed())
+ .setMinApi(AndroidApiLevel.R)
+ .compile()
+ .writeToZip();
+
+ // Run merge step with DEX with white space in input (not forcing min API level of R).
+ testForD8(parameters.getBackend())
+ .addProgramFiles(dex)
+ .setMinApi(parameters.getApiLevel())
+ .applyIf(
+ parameters.getApiLevel().isLessThan(AndroidApiLevel.R),
+ b -> {
+ try {
+ // TODO(b/269089718): This should not be an AssertionError but a compilation error.
+ b.compileWithExpectedDiagnostics(
+ diagnostics ->
+ diagnostics.assertErrorsMatch(diagnosticException(AssertionError.class)));
+ fail("Unexpected success");
+ } catch (CompilationFailedException e) {
+ // Expected.
+ }
+ },
+ b ->
+ b.run(parameters.getRuntime(), TestClass.class)
+ .assertSuccessWithOutput(EXPECTED_OUTPUT));
+ }
+
+ @Test
+ public void testD8RunWithSpaces() throws Exception {
+ assumeTrue(parameters.getApiLevel().getDexVersion().isGreaterThanOrEqualTo(DexVersion.V39));
+ testForD8(parameters.getBackend())
+ .addProgramClassFileData(getTransformed())
+ .setMinApi(AndroidApiLevel.R)
+ .run(parameters.getRuntime(), TestClass.class)
+ .applyIf(
+ parameters.getApiLevel().isLessThan(AndroidApiLevel.R),
+ b -> b.assertFailureWithErrorThatMatches(containsString("Failure to verify dex file")),
+ b -> b.assertSuccessWithOutput(EXPECTED_OUTPUT));
+ }
+
+ @Test
+ public void testD8RunWithSpacesUsingDexV40() throws Exception {
+ assumeTrue(parameters.getApiLevel().getDexVersion().isGreaterThanOrEqualTo(DexVersion.V39));
+ testForD8(parameters.getBackend())
+ .addOptionsModification(options -> options.testing.dexVersion40FromApiLevel30 = true)
+ .addProgramClassFileData(getTransformed())
+ .setMinApi(AndroidApiLevel.R)
+ .run(parameters.getRuntime(), TestClass.class)
+ .applyIf(
+ parameters.getApiLevel().isLessThan(AndroidApiLevel.R),
+ b ->
+ b.assertFailureWithErrorThatMatches(
+ allOf(containsString("Unrecognized version"), containsString("0 4 0"))),
+ b -> b.assertSuccessWithOutput(EXPECTED_OUTPUT));
+ }
+
+ private String rename(String name) {
+ return new String(Character.toChars(Integer.parseInt(name.substring(1), 16)));
+ }
+
+ private byte[] getTransformed() throws Exception {
+ return transformer(TestClass.class)
+ .renameMethod(MethodPredicate.onName(name -> name.startsWith("t")), this::rename)
+ .transformMethodInsnInMethod(
+ "main",
+ ((opcode, owner, name, descriptor, isInterface, continuation) -> {
+ continuation.visitMethodInsn(
+ opcode,
+ owner,
+ name.startsWith("t") ? rename(name) : name,
+ descriptor,
+ isInterface);
+ }))
+ .transform();
+ }
+
+ // Test with white space characters added in https://r8-review.git.corp.google.com/c/r8/+/42269.
+ // Supported on Art in https://android-review.git.corp.google.com/c/platform/art/+/1106719.
+ static class TestClass {
+
+ private static void t20() {
+ System.out.println("0x20");
+ }
+
+ private static void ta0() {
+ System.out.println("0xa0");
+ }
+
+ private static void t1680() {
+ System.out.println("0x1680");
+ }
+
+ private static void t2000() {
+ System.out.println("0x2000");
+ }
+
+ private static void t2001() {
+ System.out.println("0x2001");
+ }
+
+ private static void t2002() {
+ System.out.println("0x2002");
+ }
+
+ private static void t2003() {
+ System.out.println("0x2003");
+ }
+
+ private static void t2004() {
+ System.out.println("0x2004");
+ }
+
+ private static void t2005() {
+ System.out.println("0x2005");
+ }
+
+ private static void t2006() {
+ System.out.println("0x2006");
+ }
+
+ private static void t2007() {
+ System.out.println("0x2007");
+ }
+
+ private static void t2008() {
+ System.out.println("0x2008");
+ }
+
+ private static void t2009() {
+ System.out.println("0x2009");
+ }
+
+ private static void t200a() {
+ System.out.println("0x200a");
+ }
+
+ private static void t202f() {
+ System.out.println("0x202f");
+ }
+
+ private static void t205f() {
+ System.out.println("0x205f");
+ }
+
+ private static void t3000() {
+ System.out.println("0x3000");
+ }
+
+ public static void main(String[] args) {
+ t20();
+ ta0();
+ t1680();
+ t2000();
+ t2001();
+ t2002();
+ t2003();
+ t2004();
+ t2005();
+ t2006();
+ t2007();
+ t2008();
+ t2009();
+ t200a();
+ t202f();
+ t205f();
+ t3000();
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index f9f8f13..0ee68d2 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -39,6 +39,7 @@
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
@@ -610,11 +611,15 @@
}
static MethodPredicate onName(String name) {
- return (access, otherName, descriptor, signature, exceptions) -> name.equals(otherName);
+ return onName(name::equals);
+ }
+
+ static MethodPredicate onName(Predicate<String> predicate) {
+ return (access, otherName, descriptor, signature, exceptions) -> predicate.test(otherName);
}
static MethodPredicate onNames(Collection<String> names) {
- return (access, otherName, descriptor, signature, exceptions) -> names.contains(otherName);
+ return onName(names::contains);
}
static MethodPredicate onNames(String... names) {
@@ -758,13 +763,19 @@
}
public ClassFileTransformer renameMethod(MethodPredicate predicate, String newName) {
+ return renameMethod(predicate, name -> newName);
+ }
+
+ public ClassFileTransformer renameMethod(
+ MethodPredicate predicate, Function<String, String> newName) {
return addClassTransformer(
new ClassTransformer() {
@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
if (predicate.test(access, name, descriptor, signature, exceptions)) {
- return super.visitMethod(access, newName, descriptor, signature, exceptions);
+ return super.visitMethod(
+ access, newName.apply(name), descriptor, signature, exceptions);
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}