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
@@ -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) }
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+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