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