Add support for reading DEX container format

Bug: b/249922554
Change-Id: I3357dd7a922f779d1f45750d95aa97dc9a3ad565
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 c741ee9..eda00f9 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -49,6 +49,8 @@
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -337,9 +339,17 @@
       for (ProgramResource input : dexSources) {
         DexReader dexReader = new DexReader(input);
         if (options.passthroughDexCode) {
-          computedMinApiLevel = validateOrComputeMinApiLevel(computedMinApiLevel, dexReader);
+          if (!options.testing.dexContainerExperiment) {
+            computedMinApiLevel = validateOrComputeMinApiLevel(computedMinApiLevel, dexReader);
+          } else {
+            assert dexReader.getDexVersion() == DexVersion.V41;
+          }
         }
-        dexParsers.add(new DexParser<>(dexReader, PROGRAM, options));
+        if (!options.testing.dexContainerExperiment) {
+          dexParsers.add(new DexParser<>(dexReader, PROGRAM, options));
+        } else {
+          addDexParsersForContainer(dexParsers, dexReader);
+        }
       }
 
       options.setMinApiLevel(computedMinApiLevel);
@@ -349,17 +359,56 @@
       // Read the DexCode items and DexProgramClass items in parallel.
       if (!options.skipReadingDexCode) {
         ApplicationReaderMap applicationReaderMap = ApplicationReaderMap.getInstance(options);
-        for (DexParser<DexProgramClass> dexParser : dexParsers) {
-          futures.add(
-              executorService.submit(
-                  () -> {
-                    dexParser.addClassDefsTo(
-                        classes::add, applicationReaderMap); // Depends on Methods, Code items etc.
-                  }));
+        if (!options.testing.dexContainerExperiment) {
+          for (DexParser<DexProgramClass> dexParser : dexParsers) {
+            futures.add(
+                executorService.submit(
+                    () -> {
+                      dexParser.addClassDefsTo(
+                          classes::add,
+                          applicationReaderMap); // Depends on Methods, Code items etc.
+                    }));
+          }
+        } else {
+          // All Dex parsers use the same DEX reader, so don't process in parallel.
+          for (int i = 0; i < dexParsers.size(); i++) {
+            dexParsers.get(i).addClassDefsTo(classes::add, applicationReaderMap);
+          }
         }
       }
     }
 
+    private void addDexParsersForContainer(
+        List<DexParser<DexProgramClass>> dexParsers, DexReader dexReader) {
+      // Find the start offsets of each dex section.
+      IntList offsets = new IntArrayList();
+      dexReader.setByteOrder();
+      int offset = 0;
+      while (offset < dexReader.end()) {
+        offsets.add(offset);
+        DexReader tmp = new DexReader(Origin.unknown(), dexReader.buffer.array(), offset);
+        assert tmp.getDexVersion() == DexVersion.V41;
+        assert dexReader.getUint(offset + Constants.HEADER_SIZE_OFFSET)
+            == Constants.TYPE_HEADER_ITEM_SIZE_V41;
+        assert dexReader.getUint(offset + Constants.HEADER_OFF_OFFSET) == offset;
+        int dataSize = dexReader.getUint(offset + Constants.DATA_SIZE_OFFSET);
+        int dataOffset = dexReader.getUint(offset + Constants.DATA_OFF_OFFSET);
+        int file_size = dexReader.getUint(offset + Constants.FILE_SIZE_OFFSET);
+        assert dataOffset == 0;
+        assert dataSize == 0;
+        offset += file_size;
+      }
+      assert offset == dexReader.end();
+      // Create a parser for the last section with string data.
+      DexParser<DexProgramClass> last =
+          new DexParser<>(dexReader, PROGRAM, options, offsets.getInt(offsets.size() - 1), null);
+      // Create a parsers for the remaining sections with reference to the string data.
+      for (int i = 0; i < offsets.size() - 1; i++) {
+        dexParsers.add(new DexParser<>(dexReader, PROGRAM, options, offsets.getInt(i), last));
+      }
+      dexParsers.add(last);
+    }
+
     private boolean includeAnnotationClass(DexProgramClass clazz) {
       if (!options.pruneNonVissibleAnnotationClasses) {
         return true;
diff --git a/src/main/java/com/android/tools/r8/dex/DexParser.java b/src/main/java/com/android/tools/r8/dex/DexParser.java
index 0d9fac5..263acfc 100644
--- a/src/main/java/com/android/tools/r8/dex/DexParser.java
+++ b/src/main/java/com/android/tools/r8/dex/DexParser.java
@@ -95,21 +95,23 @@
   private final int NO_INDEX = -1;
   private final Origin origin;
   private DexReader dexReader;
-  private final DexSection[] dexSections;
+  private final List<DexSection> dexSections;
+  private final int offset;
   private int[] stringIDs;
   private final ClassKind<T> classKind;
   private final InternalOptions options;
   private Object2LongMap<String> checksums;
 
-  public static DexSection[] parseMapFrom(Path file) throws IOException {
+  public static List<DexSection> parseMapFrom(Path file) throws IOException {
     return parseMapFrom(Files.newInputStream(file), new PathOrigin(file));
   }
 
-  public static DexSection[] parseMapFrom(InputStream stream, Origin origin) throws IOException {
+  public static List<DexSection> parseMapFrom(InputStream stream, Origin origin)
+      throws IOException {
     return parseMapFrom(new DexReader(origin, ByteStreams.toByteArray(stream)));
   }
 
-  private static DexSection[] parseMapFrom(DexReader dexReader) {
+  private static List<DexSection> parseMapFrom(DexReader dexReader) {
     DexParser<DexProgramClass> dexParser =
         new DexParser<>(dexReader, ClassKind.PROGRAM, new InternalOptions());
     return dexParser.dexSections;
@@ -144,13 +146,27 @@
   private final DexItemFactory dexItemFactory;
 
   public DexParser(DexReader dexReader, ClassKind<T> classKind, InternalOptions options) {
+    this(dexReader, classKind, options, 0, null);
+  }
+
+  public DexParser(
+      DexReader dexReader,
+      ClassKind<T> classKind,
+      InternalOptions options,
+      int offset,
+      DexParser<T> parserWithStringIDs) {
     assert dexReader.getOrigin() != null;
     this.origin = dexReader.getOrigin();
     this.dexReader = dexReader;
+    this.offset = offset;
     this.dexItemFactory = options.itemFactory;
     dexReader.setByteOrder();
     dexSections = parseMap();
-    parseStringIDs();
+    if (parserWithStringIDs == null) {
+      parseStringIDs();
+    } else {
+      stringIDs = parserWithStringIDs.stringIDs;
+    }
     this.classKind = classKind;
     this.options = options;
   }
@@ -919,12 +935,12 @@
     return new DexSection(type, 0, 0, 0);
   }
 
-  private DexSection[] parseMap() {
+  private List<DexSection> parseMap() {
     // Read the dexSections information from the MAP.
-    int mapOffset = dexReader.getUint(Constants.MAP_OFF_OFFSET);
+    int mapOffset = dexReader.getUint(offset + Constants.MAP_OFF_OFFSET);
     dexReader.position(mapOffset);
     int mapSize = dexReader.getUint();
-    final DexSection[] result = new DexSection[mapSize];
+    final List<DexSection> result = new ArrayList<>(mapSize);
     for (int i = 0; i < mapSize; i++) {
       int type = dexReader.getUshort();
       int unused = dexReader.getUshort();
@@ -943,12 +959,12 @@
                 + dexReader.end(),
             origin);
       }
-      result[i] = new DexSection(type, unused, size, offset);
+      result.add(new DexSection(type, unused, size, offset));
     }
     for (int i = 0; i < mapSize - 1; i++) {
-      result[i].setEnd(result[i + 1].offset);
+      result.get(i).setEnd(result.get(i + 1).offset);
     }
-    result[mapSize - 1].setEnd(dexReader.end());
+    result.get(mapSize - 1).setEnd(dexReader.end());
     return result;
   }
 
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 18c0beb..87829fe 100644
--- a/src/main/java/com/android/tools/r8/dex/DexReader.java
+++ b/src/main/java/com/android/tools/r8/dex/DexReader.java
@@ -25,7 +25,7 @@
 
   public DexReader(ProgramResource resource) throws ResourceException, IOException {
     super(resource);
-    version = parseMagic(buffer);
+    version = parseMagic(buffer, 0);
   }
 
   /**
@@ -35,18 +35,23 @@
    */
   DexReader(Origin origin, byte[] bytes) {
     super(origin, bytes);
-    version = parseMagic(buffer);
+    version = parseMagic(buffer, 0);
+  }
+
+  DexReader(Origin origin, byte[] bytes, int offset) {
+    super(origin, bytes);
+    version = parseMagic(buffer, offset);
   }
 
   // Parse the magic header and determine the dex file version.
-  private DexVersion parseMagic(CompatByteBuffer buffer) {
+  private DexVersion parseMagic(CompatByteBuffer buffer, int offset) {
     try {
       buffer.get();
       buffer.rewind();
     } catch (BufferUnderflowException e) {
       throw new CompilationError("Dex file is empty", origin);
     }
-    int index = 0;
+    int index = offset;
     for (byte prefixByte : DEX_FILE_MAGIC_PREFIX) {
       byte actualByte = buffer.get(index++);
       if (actualByte != prefixByte) {
diff --git a/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java b/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
index 455bfe4..0373c01 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.List;
 import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -34,7 +35,7 @@
   }
 
   private int getNumberOfDebugInfos(Path file) throws IOException {
-    DexSection[] dexSections = DexParser.parseMapFrom(file);
+    List<DexSection> dexSections = DexParser.parseMapFrom(file);
     for (DexSection dexSection : dexSections) {
       if (dexSection.type == Constants.TYPE_DEBUG_INFO_ITEM) {
         return dexSection.length;
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 28a01e0..25b028a 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
@@ -33,6 +33,7 @@
 import com.android.tools.r8.utils.BitUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.DexVersion;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.io.ByteStreams;
@@ -106,7 +107,7 @@
   }
 
   @Test
-  public void testD8Experiment() throws Exception {
+  public void testD8ExperimentSimpleMerge() throws Exception {
     Path outputFromDexing =
         testForD8(Backend.DEX)
             .addProgramFiles(inputA)
@@ -116,10 +117,23 @@
             .compile()
             .writeToZip();
     validateSingleContainerDex(outputFromDexing);
+
+    Path outputFromMerging =
+        testForD8(Backend.DEX)
+            .addProgramFiles(outputFromDexing)
+            .addOptionsModification(
+                options -> options.getTestingOptions().dexContainerExperiment = true)
+            .compile()
+            .writeToZip();
+    validateSingleContainerDex(outputFromMerging);
+
+    // Identical DEX after re-merging.
+    assertArrayEquals(
+        unzipContent(outputFromDexing).get(0), unzipContent(outputFromMerging).get(0));
   }
 
   @Test
-  public void testD8Experiment2() throws Exception {
+  public void testD8ExperimentMoreMerge() throws Exception {
     Path outputA =
         testForD8(Backend.DEX)
             .addProgramFiles(inputA)
@@ -139,6 +153,27 @@
             .compile()
             .writeToZip();
     validateSingleContainerDex(outputB);
+
+    Path outputBoth =
+        testForD8(Backend.DEX)
+            .addProgramFiles(inputA, inputB)
+            .setMinApi(AndroidApiLevel.L)
+            .addOptionsModification(
+                options -> options.getTestingOptions().dexContainerExperiment = true)
+            .compile()
+            .writeToZip();
+    validateSingleContainerDex(outputBoth);
+
+    Path outputMerged =
+        testForD8(Backend.DEX)
+            .addProgramFiles(outputA, outputB)
+            .addOptionsModification(
+                options -> options.getTestingOptions().dexContainerExperiment = true)
+            .compile()
+            .writeToZip();
+    validateSingleContainerDex(outputMerged);
+
+    assertArrayEquals(unzipContent(outputBoth).get(0), unzipContent(outputMerged).get(0));
   }
 
   private void validateDex(Path output, int expectedDexes, DexVersion expectedVersion)
@@ -168,10 +203,14 @@
       int dataSize = buffer.getInt(offset + DATA_SIZE_OFFSET);
       int dataOffset = buffer.getInt(offset + DATA_OFF_OFFSET);
       int file_size = buffer.getInt(offset + FILE_SIZE_OFFSET);
-      if (!expectedVersion.isContainerDex()) {
+      if (expectedVersion.isContainerDex()) {
+        assertEquals(0, dataSize);
+        assertEquals(0, dataOffset);
+      } else {
         assertEquals(file_size, dataOffset + dataSize);
       }
       offset += expectedVersion.isContainerDex() ? file_size : dataOffset + dataSize;
+      assertEquals(file_size, offset - ListUtils.last(sections));
     }
     assertEquals(buffer.capacity(), offset);