Package the Rust mobile core and generated Kotlin API for Android (#92)
Dependency security audit / rustsec (push) Failing after 24s

This commit is contained in:
hugo committed 2026-10-08 12:34:09 +02:00
1 parent e079b82b15
commit fcb36a0684
42 files changed
+1586 -172

No files matched your search

+132
View File
@@ -1,3 +1,4 @@
import java.io.File
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@@ -10,6 +11,8 @@ android {
ndkVersion = libs.versions.ndk.get()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@@ -28,5 +31,134 @@ kotlin {
}
dependencies {
implementation("${libs.jna.get().module}:${libs.versions.jna.get()}@aar")
implementation(libs.annotation)
testImplementation(libs.junit)
androidTestImplementation(libs.android.test.runner)
}
val workspace = rootProject.layout.projectDirectory.dir("..").asFile
val cargoExecutable = providers.environmentVariable("CARGO").orElse(
providers.environmentVariable("PATH").map { path ->
path.split(File.pathSeparatorChar).map { File(it, "cargo") }
.firstOrNull { it.isFile && it.canExecute() }?.absolutePath
?: error("Cargo must be installed and in PATH")
}
)
val rustVersion = libs.versions.rust.get()
val ndkDirectory = androidComponents.sdkComponents.ndkDirectory
val sdkDirectory = androidComponents.sdkComponents.sdkDirectory
val rustInputs = files(
workspace.resolve("Cargo.toml"),
workspace.resolve("Cargo.lock"),
rootProject.file("rust-toolchain.toml"),
fileTree(workspace.resolve("crates")) { include("**/*.rs", "**/Cargo.toml", "**/uniffi.toml") }
)
val checkRustToolchain = tasks.register<Exec>("checkRustToolchain") {
workingDir = workspace
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--", "check-toolchain",
cargoExecutable.get(), rustVersion, ndkDirectory.get().asFile.absolutePath,
libs.versions.ndk.get(), libs.versions.cargoNdk.get()
)
}
androidComponents.onVariants { variant ->
val name = variant.name
val title = name.replaceFirstChar { it.uppercaseChar() }
val rawNative = layout.buildDirectory.dir("generated/native-raw/$name/jniLibs")
val nativeOutput = layout.buildDirectory.dir("generated/native/$name/jniLibs")
val rawBindings = layout.buildDirectory.dir("generated/bindings-raw/$name")
val bindings = layout.buildDirectory.dir("generated/bindings/$name")
val native = tasks.register<Exec>("build${title}Rust") {
dependsOn(checkRustToolchain)
workingDir = workspace
inputs.files(rustInputs)
inputs.property("rustVersion", rustVersion)
inputs.property("cargoNdk", libs.versions.cargoNdk.get())
inputs.property("ndkVersion", libs.versions.ndk.get())
inputs.property("minSdk", libs.versions.minSdk.get())
outputs.dir(rawNative)
environment("ANDROID_NDK_HOME", ndkDirectory.get().asFile.absolutePath)
environment("ANDROID_HOME", sdkDirectory.get().asFile.absolutePath)
environment(
"RUSTFLAGS",
"-D warnings -C link-arg=-Wl,-z,max-page-size=16384 -C link-arg=-Wl,-z,common-page-size=16384"
)
environment("CARGO_PROFILE_RELEASE_DEBUG", "2")
commandLine(
listOf(
cargoExecutable.get(), "+$rustVersion", "ndk",
"--target", "arm64-v8a",
"--target", "armeabi-v7a",
"--target", "x86_64",
"--platform", libs.versions.minSdk.get(),
"--output-dir", rawNative.get().asFile.absolutePath,
"build", "--locked", "--package", "ironstorage-android", "--lib"
) +
if (variant.buildType == "release") listOf("--release") else emptyList()
)
}
val audit = tasks.register<Exec>("package${title}Rust") {
dependsOn(native)
workingDir = workspace
inputs.dir(rawNative)
inputs.files(
fileTree(workspace.resolve("tools/android-build")) {
include("**/*.rs", "Cargo.toml")
}
)
outputs.dir(nativeOutput)
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--", "package-native",
rawNative.get().asFile.absolutePath, nativeOutput.get().asFile.absolutePath
)
}
val generate = tasks.register<Exec>("generate${title}RustBindings") {
dependsOn(audit)
workingDir = workspace
inputs.dir(nativeOutput)
inputs.file(rootProject.file("uniffi.toml"))
inputs.files(
fileTree(workspace.resolve("tools/android-build")) {
include("**/*.rs", "Cargo.toml")
}
)
outputs.dir(rawBindings)
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--bin", "ironstorage-uniffi-bindgen",
"--", "generate", "--library",
nativeOutput.get().file(
"arm64-v8a/libironstorage_android.so"
).asFile.absolutePath,
"--language", "kotlin", "--config",
rootProject.file(
"uniffi.toml"
).absolutePath,
"--out-dir", rawBindings.get().asFile.absolutePath, "--no-format"
)
}
val prepare = tasks.register<Exec>("prepare${title}RustBindings") {
dependsOn(generate)
workingDir = workspace
inputs.dir(rawBindings)
inputs.files(
fileTree(workspace.resolve("tools/android-build")) {
include("**/*.rs", "Cargo.toml")
}
)
outputs.dir(bindings)
commandLine(
cargoExecutable.get(), "+$rustVersion", "run", "--locked",
"--package", "ironstorage-android-build", "--", "prepare-bindings",
rawBindings.get().asFile.absolutePath, bindings.get().asFile.absolutePath
)
}
variant.sources.kotlin!!.addStaticSourceDirectory(bindings.get().asFile.absolutePath)
variant.sources.jniLibs!!.addStaticSourceDirectory(nativeOutput.get().asFile.absolutePath)
tasks.matching {
it.name == "pre${title}Build" || it.name == "compile${title}Kotlin"
}.configureEach { dependsOn(prepare) }
}
+7 -1
View File
@@ -1 +1,7 @@
# Native/generated keep rules are added with the UniFFI packaging in #92.
-keep class com.sun.jna.** { *; }
-keep class de.rfc1437.ironstorage.bridge.generated.** { *; }
# JNA includes optional desktop AWT helpers that Android never invokes.
-dontwarn java.awt.Component
-dontwarn java.awt.GraphicsEnvironment
-dontwarn java.awt.HeadlessException
-dontwarn java.awt.Window
+20 -13
View File
@@ -2,6 +2,12 @@
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
# To regenerate this file, run: ./gradlew :rust-bridge:dependencies --write-locks
androidx.annotation:annotation-jvm:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.annotation:annotation:1.11.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
androidx.test.services:storage:1.6.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.test:monitor:1.8.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.test:runner:1.7.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
androidx.tracing:tracing:1.1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath
ch.qos.logback:logback-classic:1.3.15=ktlint
ch.qos.logback:logback-core:1.3.15=ktlint
com.android.tools.analytics-library:protos:32.4.1=androidLintTool
@@ -73,8 +79,9 @@ io.github.oshai:kotlin-logging:5.1.0=ktlint,ktlintBaselineReporter,ktlintReporte
jakarta.activation:jakarta.activation-api:1.2.1=androidLintTool
jakarta.xml.bind:jakarta.xml.bind-api:2.3.2=androidLintTool
javax.inject:javax.inject:1=androidLintTool
junit:junit:4.13.2=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
junit:junit:4.13.2=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
net.java.dev.jna:jna-platform:5.6.0=androidLintTool
net.java.dev.jna:jna:5.19.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
net.java.dev.jna:jna:5.6.0=androidLintTool
net.sf.kxml:kxml2:2.3.0=androidLintTool
org.apache.commons:commons-compress:1.27.1=androidLintTool
@@ -91,29 +98,29 @@ org.checkerframework:checker-qual:3.43.0=androidLintTool
org.ec4j.core:ec4j-core:1.1.1=ktlint,ktlintBaselineReporter,ktlintRuleset
org.glassfish.jaxb:jaxb-runtime:2.3.2=androidLintTool
org.glassfish.jaxb:txw2:2.3.2=androidLintTool
org.hamcrest:hamcrest-core:1.3=debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.hamcrest:hamcrest-core:1.3=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-api:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-compat:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-cri-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-build-tools-impl:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-compiler-runner:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-client:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-reflect:1.6.10=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-reflect:2.2.10=androidLintTool
org.jetbrains.kotlin:kotlin-script-runtime:2.2.0=ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-script-runtime:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.20=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:2.4.21=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=ktlintReporter
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
@@ -122,8 +129,8 @@ org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.10=androidLintTool,unified-test-plat
org.jetbrains.kotlin:kotlin-stdlib:2.2.0=ktlint,ktlintBaselineReporter,ktlintReporter,ktlintRuleset
org.jetbrains.kotlin:kotlin-stdlib:2.2.10=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlin:kotlin-stdlib:2.4.0=kotlinAbiValidationCompatClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.20=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.20=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlin:kotlin-stdlib:2.4.21=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
org.jetbrains.kotlin:kotlin-tooling-core:2.4.21=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.0=kotlinAbiValidationCompatClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,ktlint,ktlintBaselineReporter,ktlintRuleset
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0=androidLintTool,unified-test-platform-gradle-work-action
@@ -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()
}
}
}
@@ -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)
}
}