Fail compilation on unsupported class file versions and DEX file versions.
Bug: 138919462
Change-Id: Ia7520a5679054d9438b2136f9ffa8e26a24e9b19
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index da04cd3..cfe4d9f 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -161,7 +161,7 @@
}
private int validateOrComputeMinApiLevel(int computedMinApiLevel, DexReader dexReader) {
- DexVersion version = DexVersion.getDexVersion(dexReader.getDexVersion());
+ DexVersion version = dexReader.getDexVersion();
if (options.minApiLevel == AndroidApiLevel.getDefault().getLevel()) {
computedMinApiLevel = Math
.max(computedMinApiLevel, AndroidApiLevel.getMinAndroidApiLevel(version).getLevel());
diff --git a/src/main/java/com/android/tools/r8/dex/DexReader.java b/src/main/java/com/android/tools/r8/dex/DexReader.java
index 45a0696..7e3f1dc 100644
--- a/src/main/java/com/android/tools/r8/dex/DexReader.java
+++ b/src/main/java/com/android/tools/r8/dex/DexReader.java
@@ -13,13 +13,14 @@
import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteOrder;
+import java.util.Optional;
/**
* {@link BinaryReader} for Dex content.
*/
public class DexReader extends BinaryReader {
- private final int version;
+ private final DexVersion version;
public DexReader(ProgramResource resource) throws ResourceException, IOException {
super(resource);
@@ -37,7 +38,7 @@
}
// Parse the magic header and determine the dex file version.
- private int parseMagic(CompatByteBuffer buffer) {
+ private DexVersion parseMagic(CompatByteBuffer buffer) {
try {
buffer.get();
buffer.rewind();
@@ -50,31 +51,24 @@
throw new CompilationError("Dex file has invalid header", origin);
}
}
- if (buffer.get(index++) != '0' || buffer.get(index++) != '3') {
- throw new CompilationError("Dex file has invalid version number", origin);
- }
- byte versionByte = buffer.get(index++);
- int version;
- switch (versionByte) {
- case '9':
- version = DexVersion.V39.getIntValue();
- break;
- case '8':
- version = DexVersion.V38.getIntValue();
- break;
- case '7':
- version = DexVersion.V37.getIntValue();
- break;
- case '5':
- version = DexVersion.V35.getIntValue();
- break;
- default:
- throw new CompilationError("Dex file has invalid version number", origin);
+
+ char versionByte0 = (char) buffer.get(index++);
+ char versionByte1 = (char) buffer.get(index++);
+ char versionByte2 = (char) buffer.get(index++);
+ Optional<DexVersion> maybeVersion =
+ DexVersion.getDexVersion(versionByte0, versionByte1, versionByte2);
+ if (!maybeVersion.isPresent()) {
+ throw new CompilationError(
+ "Unsupported DEX file version: "
+ + versionByte0
+ + versionByte1
+ + versionByte2,
+ origin);
}
if (buffer.get(index++) != '\0') {
throw new CompilationError("Dex file has invalid header", origin);
}
- return version;
+ return maybeVersion.get();
}
@Override
@@ -91,7 +85,7 @@
}
}
- int getDexVersion() {
+ DexVersion getDexVersion() {
return version;
}
}
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 34b12ea..59a2f55 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -755,9 +755,10 @@
dest.moveTo(0);
dest.putBytes(Constants.DEX_FILE_MAGIC_PREFIX);
dest.putBytes(
- DexVersion
- .getDexVersion(AndroidApiLevel.getAndroidApiLevel(options.minApiLevel))
- .getBytes());
+ options.testing.forceDexVersionBytes != null
+ ? options.testing.forceDexVersionBytes
+ : DexVersion.getDexVersion(AndroidApiLevel.getAndroidApiLevel(options.minApiLevel))
+ .getBytes());
dest.putByte(Constants.DEX_FILE_MAGIC_SUFFIX);
// Leave out checksum and signature for now.
dest.moveTo(Constants.FILE_SIZE_OFFSET);
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index 3ccc362..eae05d2 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -305,6 +305,9 @@
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
this.version = version;
+ if (InternalOptions.SUPPORTED_CF_MAJOR_VERSION < getMajorVersion()) {
+ throw new CompilationError("Unsupported class file version: " + getMajorVersion(), origin);
+ }
accessFlags = ClassAccessFlags.fromCfAccessFlags(cleanAccessFlags(access));
type = application.getTypeFromName(name);
// Check if constraints from
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 f4da3a8..99098d8 100644
--- a/src/main/java/com/android/tools/r8/utils/DexVersion.java
+++ b/src/main/java/com/android/tools/r8/utils/DexVersion.java
@@ -4,6 +4,7 @@
package com.android.tools.r8.utils;
import com.android.tools.r8.errors.Unreachable;
+import java.util.Optional;
/**
* Android dex version
@@ -75,18 +76,25 @@
}
}
- public static DexVersion getDexVersion(int intValue) {
+ public static Optional<DexVersion> getDexVersion(int intValue) {
switch (intValue) {
case 35:
- return V35;
+ return Optional.of(V35);
case 37:
- return V37;
+ return Optional.of(V37);
case 38:
- return V38;
+ return Optional.of(V38);
case 39:
- return V39;
+ return Optional.of(V39);
default:
- throw new Unreachable();
+ return Optional.empty();
}
}
+
+ public static Optional<DexVersion> getDexVersion(char b0, char b1, char b2) {
+ if (b0 != '0' || b1 != '3' || b2 < '5' || '9' < b2) {
+ return Optional.empty();
+ }
+ return getDexVersion(100 * (b0 - '0') + 10 * (b1 - '0') + (b2 - '0'));
+ }
}
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 7e788e1..cccd363 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -72,6 +72,10 @@
ON
}
+ public static final int SUPPORTED_CF_MAJOR_VERSION = Opcodes.V11;
+ public static final int SUPPORTED_DEX_VERSION =
+ AndroidApiLevel.LATEST.getDexVersion().getIntValue();
+
public static final int ASM_VERSION = Opcodes.ASM7;
public final DexItemFactory itemFactory;
@@ -909,6 +913,9 @@
public static class TestingOptions {
+ // Force writing the specified bytes as the DEX version content.
+ public byte[] forceDexVersionBytes = null;
+
public IROrdering irOrdering =
InternalOptions.assertionsEnabled() && !InternalOptions.DETERMINISTIC_DEBUGGING
? NondeterministicIROrdering.getInstance()
diff --git a/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java b/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java
new file mode 100644
index 0000000..6ea338c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/FailCompilationOnFutureVersionsTest.java
@@ -0,0 +1,122 @@
+// Copyright (c) 2019, 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;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringContains.containsString;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.utils.InternalOptions;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class FailCompilationOnFutureVersionsTest extends TestBase {
+
+ static final int UNSUPPORTED_CF_VERSION = InternalOptions.SUPPORTED_CF_MAJOR_VERSION + 1;
+ static final int UNSUPPORTED_DEX_VERSION = InternalOptions.SUPPORTED_DEX_VERSION + 1;
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+ }
+
+ public FailCompilationOnFutureVersionsTest(TestParameters parameters) {
+ this.parameters = parameters;
+ assertTrue("Test assumes the first DEX version char is '0'.", UNSUPPORTED_DEX_VERSION < 100);
+ }
+
+ @Test
+ public void testDex() throws CompilationFailedException, IOException {
+ // Generate a DEX file with a version higher than the supported one.
+ Path out =
+ testForD8()
+ .addProgramClasses(TestClass.class)
+ .setMinApi(parameters.getApiLevel())
+ .addOptionsModification(
+ options ->
+ options.testing.forceDexVersionBytes =
+ ("0" + UNSUPPORTED_DEX_VERSION).getBytes())
+ .compile()
+ .writeToZip();
+ try {
+ testForD8()
+ .addProgramFiles(out)
+ .setMinApi(parameters.getApiLevel())
+ .compileWithExpectedDiagnostics(
+ diagnotics -> {
+ diagnotics.assertOnlyErrors();
+ diagnotics.assertErrorsCount(1);
+ assertThat(
+ diagnotics.getErrors().get(0).getDiagnosticMessage(),
+ containsString("Unsupported DEX file version: 0" + UNSUPPORTED_DEX_VERSION));
+ });
+ } catch (CompilationFailedException e) {
+ return;
+ }
+ fail("Expected compilation error");
+ }
+
+ @Test
+ public void testCf() {
+ try {
+ testForD8()
+ .addProgramClassFileData(CfDump.dump())
+ .setMinApi(parameters.getApiLevel())
+ .compileWithExpectedDiagnostics(
+ diagnotics -> {
+ diagnotics.assertOnlyErrors();
+ diagnotics.assertErrorsCount(1);
+ assertThat(
+ diagnotics.getErrors().get(0).getDiagnosticMessage(),
+ containsString("Unsupported class file version: " + UNSUPPORTED_CF_VERSION));
+ assertTrue(
+ diagnotics.getErrors().stream()
+ .allMatch(
+ s ->
+ s.getDiagnosticMessage()
+ .toLowerCase()
+ .contains("unsupported class file version")));
+ });
+ } catch (CompilationFailedException e) {
+ return;
+ }
+ fail("Expected compilation error");
+ }
+
+ public static class CfDump implements Opcodes {
+
+ public static byte[] dump() {
+ // Generate a class file with a version higher than the supported one.
+ ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
+ MethodVisitor mv;
+ cw.visit(
+ UNSUPPORTED_CF_VERSION, ACC_PUBLIC + ACC_SUPER, "Test", null, "java/lang/Object", null);
+ {
+ mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+ mv.visitCode();
+ mv.visitVarInsn(ALOAD, 0);
+ mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+ mv.visitInsn(RETURN);
+ mv.visitMaxs(1, 1);
+ mv.visitEnd();
+ }
+ cw.visitEnd();
+ return cw.toByteArray();
+ }
+ }
+
+ public static class TestClass {
+ // Intentionally empty stub class for the DEX generation.
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/DexVersionUnitTests.java b/src/test/java/com/android/tools/r8/utils/DexVersionUnitTests.java
new file mode 100644
index 0000000..7174d0e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/DexVersionUnitTests.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2019, 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.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class DexVersionUnitTests extends TestBase {
+
+ private final TestParameters parameters;
+
+ @Parameterized.Parameters(name = "{0}")
+ public static TestParametersCollection data() {
+ return getTestParameters().withNoneRuntime().build();
+ }
+
+ public DexVersionUnitTests(TestParameters parameters) {
+ this.parameters = parameters;
+ }
+
+ @Test
+ public void testIntValue() {
+ assertFalse(DexVersion.getDexVersion(34).isPresent());
+ assertEquals(DexVersion.V35, DexVersion.getDexVersion(35).get());
+ assertEquals(DexVersion.V39, DexVersion.getDexVersion(39).get());
+ assertFalse(DexVersion.getDexVersion(999).isPresent());
+ }
+
+ @Test
+ public void testCharValues() {
+ assertFalse(DexVersion.getDexVersion('0', '3', '4').isPresent());
+ assertEquals(DexVersion.V35, DexVersion.getDexVersion('0', '3', '5').get());
+ assertEquals(DexVersion.V39, DexVersion.getDexVersion('0', '3', '9').get());
+ assertFalse(DexVersion.getDexVersion('9', '9', '9').isPresent());
+ }
+}
\ No newline at end of file