Use a custom sqlite-jdbc native library loader.

* Update sqlite-jdbc to 3.34.0
* Use a custom loader in Room instead of SQLiteJDBCLoader since it
workarounds current issues in the loading strategy,
specifically: https://github.com/xerial/sqlite-jdbc/pull/578.

Fixes: 177673291
Test: ./gradlew bOS
Change-Id: Idc597b734bc4d7bc03ffe938d2306b31193ada27
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b695822..adecbc3 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -131,7 +131,7 @@
 shadow = { module = "com.github.jengelman.gradle.plugins:shadow", version = "6.1.0" }
 sqldelightAndroid = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" }
 sqldelightCoroutinesExt = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" }
-sqliteJdbc = { module = "org.xerial:sqlite-jdbc", version = "3.25.2" }
+sqliteJdbc = { module = "org.xerial:sqlite-jdbc", version = "3.34.0" }
 testCore = { module = "androidx.test:core", version.ref = "androidxTest" }
 testExtJunit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExt" }
 testExtJunitKtx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidxTestExt" }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt b/room/room-compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt
index d7fd403..b435de1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/verifier/DatabaseVerifier.kt
@@ -26,7 +26,6 @@
 import androidx.room.vo.Warning
 import columnInfo
 import org.sqlite.JDBC
-import org.sqlite.SQLiteJDBCLoader
 import java.io.File
 import java.sql.Connection
 import java.sql.SQLException
@@ -68,7 +67,7 @@
             // multiple library versions, process isolation and multiple class loaders by using
             // UUID named library files.
             synchronized(System::class.java) {
-                SQLiteJDBCLoader.initialize() // extract and loads native library
+                NativeSQLiteLoader.load() // extract and loads native library
                 JDBC.isValidURL(CONNECTION_URL) // call to register driver
             }
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/verifier/NativeSQLiteLoader.kt b/room/room-compiler/src/main/kotlin/androidx/room/verifier/NativeSQLiteLoader.kt
new file mode 100644
index 0000000..677411c
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/verifier/NativeSQLiteLoader.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.verifier
+
+import org.sqlite.SQLiteJDBCLoader
+import org.sqlite.util.OSInfo
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.util.UUID
+
+/**
+ * A custom sqlite-jdbc native library extractor and loader.
+ *
+ * This class is used instead of [SQLiteJDBCLoader.initialize] since it workarounds current issues
+ * in the loading strategy, specifically: https://github.com/xerial/sqlite-jdbc/pull/578.
+ */
+internal object NativeSQLiteLoader {
+
+    private var loaded = false
+
+    private val tempDir: File by lazy {
+        File(System.getProperty("org.sqlite.tmpdir", System.getProperty("java.io.tmpdir")))
+    }
+
+    private val version: String by lazy { SQLiteJDBCLoader.getVersion() }
+
+    @JvmStatic
+    fun load() = synchronized(loaded) {
+        if (loaded) return
+        try {
+            // Cleanup target temporary folder for a new extraction.
+            cleanupTempFolder()
+            // Extract and load native library.
+            loadNativeLibrary()
+            // Reflect into original loader and mark library as extracted.
+            SQLiteJDBCLoader::class.java.getDeclaredField("extracted")
+                .apply { trySetAccessible() }
+                .set(null, true)
+        } catch (ex: Exception) {
+            // Fallback to main library if our attempt failed, do print error juuust in case, so if
+            // there is an error with our approach we get to know, instead of fully swallowing it.
+            RuntimeException("Failed to load native SQLite library, will try again though.", ex)
+                .printStackTrace()
+            SQLiteJDBCLoader.initialize()
+        }
+        loaded = true
+    }
+
+    private fun cleanupTempFolder() {
+        tempDir.listFiles { file ->
+            file.name.startsWith("sqlite-$version") && !file.name.endsWith(".lck")
+        }?.forEach { libFile ->
+            val lckFile = File(libFile.absolutePath + ".lck")
+            if (!lckFile.exists()) {
+                libFile.delete()
+            }
+        }
+    }
+
+    // Load the OS-dependent library from the Jar file.
+    private fun loadNativeLibrary() {
+        val packagePath =
+            SQLiteJDBCLoader::class.java.getPackage().name.replace(".", "/")
+        val nativeLibraryPath =
+            "/$packagePath/native/${OSInfo.getNativeLibFolderPathForCurrentOS()}"
+        val nativeLibraryName = let {
+            val libName = System.mapLibraryName("sqlitejdbc")
+                .apply { replace("dylib", "jnilib") }
+            if (hasResource("$nativeLibraryPath/$libName")) {
+                return@let libName
+            }
+            if (OSInfo.getOSName() == "Mac") {
+                // Fix for openjdk7 for Mac
+                val altLibName = "libsqlitejdbc.jnilib"
+                if (hasResource("$nativeLibraryPath/$altLibName")) {
+                    return@let altLibName
+                }
+            }
+            error(
+                "No native library is found for os.name=${OSInfo.getOSName()} and " +
+                    "os.arch=${OSInfo.getArchName()}. path=$nativeLibraryPath"
+            )
+        }
+
+        val extractedNativeLibraryFile = try {
+            extractNativeLibrary(nativeLibraryPath, nativeLibraryName, tempDir.absolutePath)
+        } catch (ex: IOException) {
+            throw RuntimeException("Couldn't extract native SQLite library.", ex)
+        }
+        try {
+            @Suppress("UnsafeDynamicallyLoadedCode") // Loading an from an absolute path.
+            System.load(extractedNativeLibraryFile.absolutePath)
+        } catch (ex: UnsatisfiedLinkError) {
+            throw RuntimeException("Couldn't load native SQLite library.", ex)
+        }
+    }
+
+    private fun extractNativeLibrary(
+        libraryPath: String,
+        libraryName: String,
+        targetDirPath: String
+    ): File {
+        val libraryFilePath = "$libraryPath/$libraryName"
+        // Include arch name in temporary filename in order to avoid conflicts when multiple JVMs
+        // with different architectures are running.
+        val outputLibraryFile = File(
+            targetDirPath,
+            "sqlite-$version-${UUID.randomUUID()}-$libraryName"
+        ).apply { deleteOnExit() }
+        val outputLibraryLckFile = File(
+            targetDirPath,
+            "${outputLibraryFile.name}.lck"
+        ).apply { deleteOnExit() }
+        if (!outputLibraryLckFile.exists()) {
+            outputLibraryLckFile.outputStream().close()
+        }
+        getResourceAsStream(libraryFilePath).use { inputStream ->
+            outputLibraryFile.outputStream().use { outputStream ->
+                inputStream.copyTo(outputStream)
+            }
+        }
+        // Set executable flag (x) to enable loading the library.
+        outputLibraryFile.setReadable(true)
+        outputLibraryFile.setExecutable(true)
+        return outputLibraryFile
+    }
+
+    private fun hasResource(path: String) = SQLiteJDBCLoader::class.java.getResource(path) != null
+
+    // Replacement of java.lang.Class#getResourceAsStream(String) to disable sharing the resource
+    // stream in multiple class loaders and specifically to avoid
+    // https://bugs.openjdk.java.net/browse/JDK-8205976
+    private fun getResourceAsStream(name: String): InputStream {
+        // Remove leading '/' since all our resource paths include a leading directory
+        // See: https://github.com/openjdk/jdk/blob/jdk-11+0/src/java.base/share/classes/java/lang/Class.java#L2573
+        val resolvedName = name.drop(1)
+        val url = SQLiteJDBCLoader::class.java.classLoader.getResource(resolvedName)
+            ?: throw IOException("Resource '$resolvedName' could not be found.")
+        return url.openConnection().apply {
+            defaultUseCaches = false
+        }.getInputStream()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/verifier/NativeSQLiteLoaderTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/verifier/NativeSQLiteLoaderTest.kt
new file mode 100644
index 0000000..8cf547b
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/verifier/NativeSQLiteLoaderTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.verifier
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+import java.net.URL
+import java.net.URLClassLoader
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.jar.JarEntry
+import java.util.jar.JarOutputStream
+
+@RunWith(JUnit4::class)
+class NativeSQLiteLoaderTest {
+
+    @Test
+    fun multipleClassLoader() {
+        // Get current classpath
+        val stringUrls = System.getProperty("java.class.path")!!
+            .split(System.getProperty("path.separator")!!).toTypedArray()
+        // Find classes under test.
+        val targetDirName = "room-compiler/build/classes/kotlin/main"
+        val classesDirPath = stringUrls.first { it.contains(targetDirName) }
+        // Create a JAR file out the classes and resources
+        val jarFile = File.createTempFile("jar-for-test-", ".jar")
+        createJar(classesDirPath, jarFile)
+        val jarUrl = URL("file://${jarFile.absolutePath}")
+        // Find Kotlin stdlibs (will need them to load class under test)
+        val kotlinStdbLibUrls = stringUrls
+            .filter { it.contains("kotlin-stdlib") && it.endsWith(".jar") }
+            .map { URL("file://$it") }
+        // Also find sqlite-jdbc since it is a hard dep of NativeSQLiteLoader
+        val sqliteJdbcJarUrl = stringUrls
+            .filter { it.contains("sqlite-jdbc") && it.endsWith(".jar") }
+            .map { URL("file://$it") }
+        // Spawn a few threads and have them all in parallel load the native lib
+        val completedThreads = AtomicInteger(0)
+        val numOfThreads = 8
+        val pool = Executors.newFixedThreadPool(numOfThreads)
+        val loadedClasses = arrayOfNulls<Class<*>>(numOfThreads)
+        for (i in 1..numOfThreads) {
+            pool.execute {
+                try {
+                    Thread.sleep((i * 10).toLong())
+                    // Create an isolated class loader, it should load *different* instances
+                    // of NativeSQLiteLoader.class
+                    val classLoader = URLClassLoader(
+                        (kotlinStdbLibUrls + sqliteJdbcJarUrl + jarUrl).toTypedArray(),
+                        ClassLoader.getSystemClassLoader().parent
+                    )
+                    val clazz =
+                        classLoader.loadClass("androidx.room.verifier.NativeSQLiteLoader")
+                    clazz.getDeclaredMethod("load").invoke(null)
+                    classLoader.close()
+                    loadedClasses[i - 1] = clazz
+                } catch (e: Throwable) {
+                    e.printStackTrace()
+                    fail(e.message)
+                }
+                completedThreads.incrementAndGet()
+            }
+        }
+        // Verify all threads completed
+        pool.shutdown()
+        pool.awaitTermination(3, TimeUnit.SECONDS)
+        assertThat(completedThreads.get()).isEqualTo(numOfThreads)
+        // Verify all loaded classes are different from each other
+        loadedClasses.forEachIndexed { i, clazz1 ->
+            loadedClasses.forEachIndexed { j, clazz2 ->
+                if (i == j) {
+                    assertThat(clazz1).isEqualTo(clazz2)
+                } else {
+                    assertThat(clazz1).isNotEqualTo(clazz2)
+                }
+            }
+        }
+    }
+
+    private fun createJar(inputDir: String, outputFile: File) {
+        JarOutputStream(outputFile.outputStream()).use {
+            addJarEntry(File(inputDir), inputDir, it)
+        }
+    }
+
+    private fun addJarEntry(source: File, changeDir: String, target: JarOutputStream) {
+        if (source.isDirectory) {
+            var name = source.path.replace("\\", "/")
+            if (name.isNotEmpty()) {
+                if (!name.endsWith("/")) {
+                    name += "/"
+                }
+                val entry = JarEntry(name.substring(changeDir.length + 1))
+                entry.time = source.lastModified()
+                target.putNextEntry(entry)
+                target.closeEntry()
+            }
+            source.listFiles()!!.forEach { nestedFile ->
+                addJarEntry(nestedFile, changeDir, target)
+            }
+        } else if (source.isFile) {
+            val entry = JarEntry(
+                source.path.replace("\\", "/").substring(changeDir.length + 1)
+            )
+            entry.time = source.lastModified()
+            target.putNextEntry(entry)
+            source.inputStream().use { inputStream ->
+                inputStream.copyTo(target)
+            }
+            target.closeEntry()
+        }
+    }
+}
\ No newline at end of file