Package the Rust mobile core and generated Kotlin API for Android (#92)
Dependency security audit / rustsec (push) Failing after 24s
Dependency security audit / rustsec (push) Failing after 24s
This commit is contained in:
1 parent
e079b82b15
commit
fcb36a0684
42 files changed
+1586
-172
No files matched your search
+128
@@ -0,0 +1,128 @@
|
||||
package de.rfc1437.ironstorage.bridge
|
||||
|
||||
import android.content.ContextWrapper
|
||||
import android.content.pm.ApplicationInfo
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import de.rfc1437.ironstorage.bridge.generated.FfiConverterString
|
||||
import de.rfc1437.ironstorage.bridge.generated.IntegrityCheckingUniffiLib
|
||||
import de.rfc1437.ironstorage.bridge.generated.InternalException
|
||||
import de.rfc1437.ironstorage.bridge.generated.MobileAuthenticationErrorKind
|
||||
import de.rfc1437.ironstorage.bridge.generated.MobileAuthenticationFfiException
|
||||
import de.rfc1437.ironstorage.bridge.generated.MobileEntryCopy
|
||||
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
|
||||
import de.rfc1437.ironstorage.bridge.generated.MobileTab
|
||||
import de.rfc1437.ironstorage.bridge.generated.UniffiRustCallStatus
|
||||
import de.rfc1437.ironstorage.bridge.generated.uniffiTraitInterfaceCall
|
||||
import java.io.File
|
||||
import java.util.zip.ZipOutputStream
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
|
||||
class NativeRuntimeTest {
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@Test
|
||||
fun generatedBindingsCallEveryRustFixtureAndRedactDiagnostics() {
|
||||
val runtime = NativeRuntime(context)
|
||||
MobileShellState.entries.forEach { state ->
|
||||
val shell = runtime.fixture(state)
|
||||
assertEquals(MobileTab.entries, shell.pages.map { it.tab })
|
||||
assertEquals(setOf(state), shell.pages.map { it.state }.toSet())
|
||||
}
|
||||
val secret = "NativeFixtureSecretMustNeverAppearInDiagnostics"
|
||||
val copy = MobileEntryCopy(secret, 60uL)
|
||||
assertEquals(secret, copy.value)
|
||||
assertFalse(copy.toString().contains(secret))
|
||||
val error = MobileAuthenticationFfiException.Failed(
|
||||
MobileAuthenticationErrorKind.entries.first(),
|
||||
secret,
|
||||
secret
|
||||
)
|
||||
assertFalse(error.toString().contains(secret))
|
||||
assertFalse(error.message.contains(secret))
|
||||
assertFalse(InternalException(secret).toString().contains(secret))
|
||||
val status = UniffiRustCallStatus()
|
||||
uniffiTraitInterfaceCall<Unit>(status, { throw IllegalStateException(secret) }, {})
|
||||
assertEquals(
|
||||
"Native callback failed ([REDACTED])",
|
||||
FfiConverterString.lift(status.error_buf)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wrongNativeContractAndChecksumFailWithTypedRedactedErrors() {
|
||||
NativeRuntime(context).fixture(MobileShellState.EMPTY)
|
||||
val version = IntegrityCheckingUniffiLib
|
||||
.ffi_ironstorage_apple_uniffi_contract_version().toLong()
|
||||
val checksum = IntegrityCheckingUniffiLib
|
||||
.uniffi_ironstorage_apple_checksum_func_mobile_shell_fixture().toLong() and 0xffffL
|
||||
NativeBindingChecks.requireMatch(-27278, 38258, NativeFailureKind.ChecksumMismatch)
|
||||
for ((actual, kind) in listOf(
|
||||
version to NativeFailureKind.ContractMismatch,
|
||||
checksum to NativeFailureKind.ChecksumMismatch
|
||||
)) {
|
||||
val failure = assertThrows(NativeBindingMismatchException::class.java) {
|
||||
NativeBindingChecks.requireMatch(actual, actual + 1, kind)
|
||||
}
|
||||
assertEquals(kind, failure.kind)
|
||||
assertEquals("Rust bindings do not match the packaged native library", failure.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mainThreadAndUnreadablePackagesFailWithTypedErrors() {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
val failure = assertThrows(NativeBridgeException::class.java) {
|
||||
NativeRuntime(context).fixture(MobileShellState.EMPTY)
|
||||
}
|
||||
assertEquals(NativeFailureKind.MainThreadCall, failure.kind)
|
||||
}
|
||||
val packageFile = File.createTempFile("native-corrupt-package", ".apk", context.cacheDir)
|
||||
try {
|
||||
val info = ApplicationInfo(context.applicationInfo).apply {
|
||||
sourceDir = packageFile.absolutePath
|
||||
splitSourceDirs = null
|
||||
nativeLibraryDir = File(context.cacheDir, "missing-native-fixture").absolutePath
|
||||
}
|
||||
val invalid = object : ContextWrapper(context) {
|
||||
override fun getApplicationContext() = this
|
||||
override fun getApplicationInfo() = info
|
||||
}
|
||||
val failure = assertThrows(NativeBridgeException::class.java) {
|
||||
NativeRuntime(invalid).fixture(MobileShellState.EMPTY)
|
||||
}
|
||||
assertEquals(NativeFailureKind.LoadFailure, failure.kind)
|
||||
} finally {
|
||||
packageFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingLibraryAndUnsupportedAbiFailBeforeEnteringRust() {
|
||||
val abiFailure = assertThrows(NativeBridgeException::class.java) {
|
||||
NativeRuntime.supportedAbi(listOf("unsupported-fixture-abi"))
|
||||
}
|
||||
assertEquals(NativeFailureKind.UnsupportedAbi, abiFailure.kind)
|
||||
val packageFile = File.createTempFile("native-empty-package", ".apk", context.cacheDir)
|
||||
try {
|
||||
ZipOutputStream(packageFile.outputStream()).close()
|
||||
val info = ApplicationInfo(context.applicationInfo).apply {
|
||||
sourceDir = packageFile.absolutePath
|
||||
splitSourceDirs = null
|
||||
nativeLibraryDir = File(context.cacheDir, "missing-native-fixture").absolutePath
|
||||
}
|
||||
val missing = object : ContextWrapper(context) {
|
||||
override fun getApplicationContext() = this
|
||||
override fun getApplicationInfo() = info
|
||||
}
|
||||
val failure = assertThrows(NativeBridgeException::class.java) {
|
||||
NativeRuntime(missing).fixture(MobileShellState.EMPTY)
|
||||
}
|
||||
assertEquals(NativeFailureKind.MissingLibrary, failure.kind)
|
||||
} finally {
|
||||
packageFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package de.rfc1437.ironstorage.bridge
|
||||
|
||||
internal object NativeBindingChecks {
|
||||
fun requireMatch(actual: Long, expected: Long, kind: NativeFailureKind) {
|
||||
// UniFFI checksums are u16; JNA's Int return can include unspecified upper bits.
|
||||
val value = if (kind == NativeFailureKind.ChecksumMismatch) actual and 0xffffL else actual
|
||||
if (value != expected) throw NativeBindingMismatchException(kind)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.rfc1437.ironstorage.bridge
|
||||
|
||||
enum class NativeFailureKind {
|
||||
UnsupportedAbi,
|
||||
MissingLibrary,
|
||||
ContractMismatch,
|
||||
ChecksumMismatch,
|
||||
LoadFailure,
|
||||
RustFailure,
|
||||
MainThreadCall
|
||||
}
|
||||
|
||||
class NativeBindingMismatchException(val kind: NativeFailureKind) :
|
||||
LinkageError("Rust bindings do not match the packaged native library")
|
||||
|
||||
class NativeBridgeException(val kind: NativeFailureKind) :
|
||||
Exception("Native Rust runtime unavailable: $kind")
|
||||
@@ -0,0 +1,80 @@
|
||||
package de.rfc1437.ironstorage.bridge
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import de.rfc1437.ironstorage.bridge.generated.MobileShell
|
||||
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
|
||||
import de.rfc1437.ironstorage.bridge.generated.mobileShellFixture
|
||||
import de.rfc1437.ironstorage.bridge.generated.uniffiEnsureInitialized
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
/** OS library loading only. Callers schedule all native calls off the main thread. */
|
||||
class NativeRuntime(context: Context) {
|
||||
private val applicationInfo = context.applicationContext.applicationInfo
|
||||
private val lock = Any()
|
||||
private var initialized = false
|
||||
private var initializationFailure: NativeFailureKind? = null
|
||||
|
||||
fun fixture(state: MobileShellState): MobileShell = call {
|
||||
initialize()
|
||||
mobileShellFixture(state)
|
||||
}
|
||||
|
||||
private fun initialize() = synchronized(lock) {
|
||||
initializationFailure?.let { throw NativeBridgeException(it) }
|
||||
if (!initialized) {
|
||||
val abi = supportedAbi(Build.SUPPORTED_ABIS.toList())
|
||||
val extracted = File(applicationInfo.nativeLibraryDir, LIBRARY_FILE).isFile
|
||||
val packages =
|
||||
listOf(applicationInfo.sourceDir) + applicationInfo.splitSourceDirs.orEmpty()
|
||||
val packaged = extracted || packages.any { path ->
|
||||
ZipFile(path).use { it.getEntry("lib/$abi/$LIBRARY_FILE") != null }
|
||||
}
|
||||
if (!packaged) throw NativeBridgeException(NativeFailureKind.MissingLibrary)
|
||||
System.loadLibrary(LIBRARY)
|
||||
uniffiEnsureInitialized()
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> call(operation: () -> T): T {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
throw NativeBridgeException(NativeFailureKind.MainThreadCall)
|
||||
}
|
||||
try {
|
||||
return operation()
|
||||
} catch (error: NativeBridgeException) {
|
||||
fail(error.kind)
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: NativeBindingMismatchException) {
|
||||
fail(error.kind)
|
||||
} catch (_: IOException) {
|
||||
fail(NativeFailureKind.LoadFailure)
|
||||
} catch (_: LinkageError) {
|
||||
fail(NativeFailureKind.LoadFailure)
|
||||
} catch (_: Exception) {
|
||||
fail(NativeFailureKind.RustFailure)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fail(kind: NativeFailureKind): Nothing {
|
||||
synchronized(lock) {
|
||||
if (!initialized) initializationFailure = kind
|
||||
}
|
||||
throw NativeBridgeException(kind)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LIBRARY = "ironstorage_android"
|
||||
private const val LIBRARY_FILE = "libironstorage_android.so"
|
||||
|
||||
internal fun supportedAbi(deviceAbis: List<String>): String =
|
||||
deviceAbis.firstOrNull { it in setOf("arm64-v8a", "armeabi-v7a", "x86_64") }
|
||||
?: throw NativeBridgeException(NativeFailureKind.UnsupportedAbi)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user