Load in-memory dex into a single DexFile/Element object

InMemoryDexClassLoader only accepts ByteBuffers containing dex files,
not apks. Loading dex files in a multidex fashion is therefore
impossible as each ByteBuffer is loaded into its own Element/DexFile
object. This has not been an issue so far because dex files loaded with
InMemoryDexClassLoader are never optimized or backed by an oat file,
and the first time a class can be resolved is after the whole class
loader class path has been initialized.

Refactor this now because introduction of a vdex backing the dex files
changes this. It is also convenient to generate just one vdex per class
loader than one vdex per ByteBuffer.

The original makeInMemoryDexElements() method is left unused after the
refactor but it is known to be in use by 3p apps. Leave it there are add
a test to make sure it continues to work.

Bug: 72131483
Test: art/tools/run-libcore-tests.sh
Change-Id: I618b286951861b3ff9a4622599244b6f5c8b4bc3
diff --git a/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java b/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
index 663a7c8..9cf5f80 100644
--- a/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
+++ b/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
@@ -172,8 +172,9 @@
      */
     public BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) {
         super(parent);
-        this.pathList = new DexPathList(this, dexFiles, librarySearchPath);
         this.sharedLibraryLoaders = null;
+        this.pathList = new DexPathList(this, librarySearchPath);
+        this.pathList.initByteBufferDexPath(dexFiles);
     }
 
     @Override
diff --git a/dalvik/src/main/java/dalvik/system/DexFile.java b/dalvik/src/main/java/dalvik/system/DexFile.java
index 535257f..8d32931 100644
--- a/dalvik/src/main/java/dalvik/system/DexFile.java
+++ b/dalvik/src/main/java/dalvik/system/DexFile.java
@@ -102,15 +102,16 @@
      * @param elements
      *            the temporary dex path list elements from DexPathList.makeElements
      */
-    DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
+    DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements)
+            throws IOException {
         mCookie = openDexFile(fileName, null, 0, loader, elements);
         mInternalCookie = mCookie;
         mFileName = fileName;
         //System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
     }
 
-    DexFile(ByteBuffer buf) throws IOException {
-        mCookie = openInMemoryDexFile(buf);
+    DexFile(ByteBuffer[] bufs) throws IOException {
+        mCookie = openInMemoryDexFiles(bufs);
         mInternalCookie = mCookie;
         mFileName = null;
     }
@@ -369,16 +370,23 @@
                                  elements);
     }
 
-    private static Object openInMemoryDexFile(ByteBuffer buf) throws IOException {
-        if (buf.isDirect()) {
-            return createCookieWithDirectBuffer(buf, buf.position(), buf.limit());
-        } else {
-            return createCookieWithArray(buf.array(), buf.position(), buf.limit());
+    private static Object openInMemoryDexFiles(ByteBuffer[] bufs) throws IOException {
+        // Preprocess the ByteBuffers for openInMemoryDexFilesNative. We extract
+        // the backing array (non-direct buffers only) and start/end positions
+        // so that the native method does not have to call Java methods anymore.
+        byte[][] arrays = new byte[bufs.length][];
+        int[] starts = new int[bufs.length];
+        int[] ends = new int[bufs.length];
+        for (int i = 0; i < bufs.length; ++i) {
+            arrays[i] = bufs[i].isDirect() ? null : bufs[i].array();
+            starts[i] = bufs[i].position();
+            ends[i] = bufs[i].limit();
         }
+        return openInMemoryDexFilesNative(bufs, arrays, starts, ends);
     }
 
-    private static native Object createCookieWithDirectBuffer(ByteBuffer buf, int start, int end);
-    private static native Object createCookieWithArray(byte[] buf, int start, int end);
+    private static native Object openInMemoryDexFilesNative(ByteBuffer[] bufs, byte[][] arrays,
+            int[] starts, int[] ends);
 
     /*
      * Returns true if the dex file is backed by a valid oat file.
diff --git a/dalvik/src/main/java/dalvik/system/DexPathList.java b/dalvik/src/main/java/dalvik/system/DexPathList.java
index 6340b77..fc57dc0 100644
--- a/dalvik/src/main/java/dalvik/system/DexPathList.java
+++ b/dalvik/src/main/java/dalvik/system/DexPathList.java
@@ -49,8 +49,10 @@
  *
  * <p>This class also contains methods to use these lists to look up
  * classes and resources.</p>
+ *
+ * @hide
  */
-/*package*/ final class DexPathList {
+public final class DexPathList {
     private static final String DEX_SUFFIX = ".dex";
     private static final String zipSeparator = "!/";
 
@@ -99,33 +101,16 @@
      *
      * @param dexFiles the bytebuffers containing the dex files that we should load classes from.
      */
-    public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles,
-            String librarySearchPath) {
+    public DexPathList(ClassLoader definingContext, String librarySearchPath) {
         if (definingContext == null) {
             throw new NullPointerException("definingContext == null");
         }
-        if (dexFiles == null) {
-            throw new NullPointerException("dexFiles == null");
-        }
-        if (Arrays.stream(dexFiles).anyMatch(v -> v == null)) {
-            throw new NullPointerException("dexFiles contains a null Buffer!");
-        }
 
         this.definingContext = definingContext;
-
         this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
         this.systemNativeLibraryDirectories =
                 splitPaths(System.getProperty("java.library.path"), true);
         this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
-
-        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
-        this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
-        if (suppressedExceptions.size() > 0) {
-            this.dexElementsSuppressedExceptions =
-                    suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
-        } else {
-            dexElementsSuppressedExceptions = null;
-        }
     }
 
     /**
@@ -261,6 +246,41 @@
     }
 
     /**
+     * For InMemoryDexClassLoader. Initializes {@code dexElements} with dex files
+     * loaded from {@code dexFiles} buffers.
+     *
+     * @param dexFiles ByteBuffers containing raw dex data. Apks are not supported.
+     */
+    /* package */ void initByteBufferDexPath(ByteBuffer[] dexFiles) {
+        if (dexFiles == null) {
+            throw new NullPointerException("dexFiles == null");
+        }
+        if (Arrays.stream(dexFiles).anyMatch(v -> v == null)) {
+            throw new NullPointerException("dexFiles contains a null Buffer!");
+        }
+        if (dexElements != null || dexElementsSuppressedExceptions != null) {
+            throw new IllegalStateException("Should only be called once");
+        }
+
+        final List<IOException> suppressedExceptions = new ArrayList<IOException>();
+
+        try {
+            Element[] null_elements = null;
+            DexFile dex = new DexFile(dexFiles);
+            dexElements = new Element[] { new Element(dex) };
+        } catch (IOException suppressed) {
+            System.logE("Unable to load dex files", suppressed);
+            suppressedExceptions.add(suppressed);
+            dexElements = new Element[0];
+        }
+
+        if (suppressedExceptions.size() > 0) {
+            dexElementsSuppressedExceptions = suppressedExceptions.toArray(
+                    new IOException[suppressedExceptions.size()]);
+        }
+    }
+
+    /**
      * Splits the given dex path string into elements using the path
      * separator, pruning out any elements that do not refer to existing
      * and readable files.
@@ -301,14 +321,16 @@
         return result;
     }
 
+    // This method is not used anymore. Kept around only because there are many legacy users of it.
+    @SuppressWarnings("unused")
     @UnsupportedAppUsage
-    private static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
+    public static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
             List<IOException> suppressedExceptions) {
         Element[] elements = new Element[dexFiles.length];
         int elementPos = 0;
         for (ByteBuffer buf : dexFiles) {
             try {
-                DexFile dex = new DexFile(buf);
+                DexFile dex = new DexFile(new ByteBuffer[] { buf });
                 elements[elementPos++] = new Element(dex);
             } catch (IOException suppressed) {
                 System.logE("Unable to load dex file: " + buf, suppressed);
diff --git a/luni/src/test/java/libcore/dalvik/system/InMemoryDexClassLoaderTest.java b/luni/src/test/java/libcore/dalvik/system/InMemoryDexClassLoaderTest.java
index ef87ee5..1012c9f 100644
--- a/luni/src/test/java/libcore/dalvik/system/InMemoryDexClassLoaderTest.java
+++ b/luni/src/test/java/libcore/dalvik/system/InMemoryDexClassLoaderTest.java
@@ -24,11 +24,13 @@
 import java.io.RandomAccessFile;
 import java.nio.ByteBuffer;
 import java.nio.file.Files;
+import java.util.ArrayList;
 import libcore.io.Streams;
 import junit.framework.TestCase;
 
 import dalvik.system.BaseDexClassLoader;
 import dalvik.system.InMemoryDexClassLoader;
+import dalvik.system.DexPathList;
 
 /**
  * Tests for the class {@link InMemoryDexClassLoader}.
@@ -424,6 +426,20 @@
         classLoader.loadClass("test.TestMethods");
     }
 
+    /**
+     * DexPathList.makeInMemoryDexElements() is a legacy code path not used by
+     * InMemoryDexClassLoader anymore but heavily used by 3p apps. Test that it still works.
+     */
+    public void testMakeInMemoryDexElements() throws Exception {
+        ArrayList<IOException> exceptions = new ArrayList<>();
+        Object[] elements = DexPathList.makeInMemoryDexElements(
+                new ByteBuffer[] { readFileToByteBufferDirect(dex1),
+                                   readFileToByteBufferIndirect(dex2) },
+                exceptions);
+        assertEquals(2, elements.length);
+        assertTrue(exceptions.isEmpty());
+    }
+
     private static File makeEmptyFile(File directory, String name) throws IOException {
         assertTrue(directory.mkdirs());
         File result = new File(directory, name);