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

@@ -0,0 +1,101 @@
package de.rfc1437.ironstorage.core
import android.content.Context
import de.rfc1437.ironstorage.bridge.NativeBridgeException
import de.rfc1437.ironstorage.bridge.NativeFailureKind
import de.rfc1437.ironstorage.bridge.NativeRuntime
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
import de.rfc1437.ironstorage.bridge.generated.MobileTab
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** #92's native fixture integration; repository initialization is owned by #90. */
class NativeFixtureShellRepository internal constructor(
private val scope: CoroutineScope,
private val load: suspend (ShellState) -> ShellSnapshot
) : ShellRepository {
constructor(context: Context, scope: CoroutineScope) : this(scope, nativeLoader(context))
private val mutableSnapshot = MutableStateFlow(ShellSnapshot(ShellState.Loading, emptyList()))
override val snapshot: StateFlow<ShellSnapshot> = mutableSnapshot
private val revision = AtomicLong()
private var job: Job? = null
init {
showFixture(ShellState.Empty)
}
override fun selectTab(tab: ShellTab) {
mutableSnapshot.update { it.copy(selectedTab = tab) }
}
fun showFixture(state: ShellState) {
val operation = revision.incrementAndGet()
job?.cancel()
job = scope.launch {
val result = try {
load(state)
} catch (error: NativeBridgeException) {
ShellSnapshot(ShellState.Error, emptyList(), nativeFailure = error.kind.toFacade())
}
if (revision.get() == operation) {
mutableSnapshot.update { result.copy(selectedTab = it.selectedTab) }
}
}
}
}
private fun nativeLoader(context: Context): suspend (ShellState) -> ShellSnapshot {
val runtime = NativeRuntime(context)
return { state ->
withContext(Dispatchers.IO) {
val shell = runtime.fixture(state.toNative())
val pages = shell.pages.map { page ->
ShellPage(page.tab.toFacade(), page.title, page.stateTitle, page.stateDetail)
}
val actual = shell.pages.firstOrNull()?.state
?: throw NativeBridgeException(NativeFailureKind.RustFailure)
ShellSnapshot(actual.toFacade(), pages)
}
}
}
private fun MobileShellState.toFacade(): ShellState = when (this) {
MobileShellState.LOADING -> ShellState.Loading
MobileShellState.EMPTY -> ShellState.Empty
MobileShellState.READY -> ShellState.Ready
MobileShellState.LOCKED -> ShellState.Locked
MobileShellState.ERROR -> ShellState.Error
}
private fun ShellState.toNative(): MobileShellState = when (this) {
ShellState.Loading -> MobileShellState.LOADING
ShellState.Empty -> MobileShellState.EMPTY
ShellState.Ready -> MobileShellState.READY
ShellState.Locked -> MobileShellState.LOCKED
ShellState.Error -> MobileShellState.ERROR
}
private fun MobileTab.toFacade(): ShellTab = when (this) {
MobileTab.HOME -> ShellTab.Home
MobileTab.PASSWORDS -> ShellTab.Passwords
MobileTab.SEARCH -> ShellTab.Search
MobileTab.TOTP -> ShellTab.Totp
MobileTab.PREFERENCES -> ShellTab.Preferences
}
private fun NativeFailureKind.toFacade(): NativeFailure = when (this) {
NativeFailureKind.UnsupportedAbi -> NativeFailure.UnsupportedAbi
NativeFailureKind.MissingLibrary -> NativeFailure.MissingLibrary
NativeFailureKind.ContractMismatch -> NativeFailure.ContractMismatch
NativeFailureKind.ChecksumMismatch -> NativeFailure.ChecksumMismatch
NativeFailureKind.LoadFailure -> NativeFailure.LoadFailure
NativeFailureKind.RustFailure -> NativeFailure.RustFailure
NativeFailureKind.MainThreadCall -> NativeFailure.MainThreadCall
}
@@ -14,9 +14,20 @@ data class ShellPage(
data class ShellSnapshot(
val state: ShellState,
val pages: List<ShellPage>,
val selectedTab: ShellTab = ShellTab.Home
val selectedTab: ShellTab = ShellTab.Home,
val nativeFailure: NativeFailure? = null
)
enum class NativeFailure {
UnsupportedAbi,
MissingLibrary,
ContractMismatch,
ChecksumMismatch,
LoadFailure,
RustFailure,
MainThreadCall
}
interface ShellRepository {
val snapshot: StateFlow<ShellSnapshot>
fun selectTab(tab: ShellTab)
@@ -0,0 +1,31 @@
package de.rfc1437.ironstorage.core
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.junit.Assert.assertEquals
import org.junit.Test
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class NativeFixtureShellRepositoryTest {
@Test
fun cancelledNativeCompletionCannotReplaceNewerStateOrTabSelection() = runTest {
val pending = CompletableDeferred<Unit>()
val repository = NativeFixtureShellRepository(backgroundScope) { state ->
if (state == ShellState.Ready) withContext(NonCancellable) { pending.await() }
ShellSnapshot(state, listOf(ShellPage(ShellTab.Home, "Home", state.name, "Fixture")))
}
runCurrent()
repository.showFixture(ShellState.Ready)
runCurrent()
repository.selectTab(ShellTab.Search)
repository.showFixture(ShellState.Locked)
runCurrent()
pending.complete(Unit)
runCurrent()
assertEquals(ShellState.Locked, repository.snapshot.value.state)
assertEquals(ShellTab.Search, repository.snapshot.value.selectedTab)
}
}