feat(android): implement private Rust storage runtime (#90)
Dependency security audit / rustsec (push) Failing after 23s

This commit is contained in:
hugo committed 2026-10-08 14:36:05 +02:00
1 parent 180851087b
commit 874c8139a4
36 files changed
+2848 -39

No files matched your search

@@ -5,8 +5,13 @@ import de.rfc1437.ironstorage.bridge.NativeBridgeException
import de.rfc1437.ironstorage.bridge.NativeFailureKind
import de.rfc1437.ironstorage.bridge.NativeRuntime
import de.rfc1437.ironstorage.bridge.generated.MobileIcon
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateDirectories
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateRuntime
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageErrorKind
import de.rfc1437.ironstorage.bridge.generated.MobilePrivateStorageFfiException
import de.rfc1437.ironstorage.bridge.generated.MobileShellState
import de.rfc1437.ironstorage.bridge.generated.MobileTab
import de.rfc1437.ironstorage.bridge.generated.mobilePrivateStorageFailure
import java.util.concurrent.atomic.AtomicLong
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -17,12 +22,13 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/** #92's native fixture integration; repository initialization is owned by #90. */
/** Rust-owned private initialization; debug controls can switch to explicit fixtures. */
class NativeFixtureShellRepository internal constructor(
private val scope: CoroutineScope,
private val load: suspend (ShellState) -> ShellSnapshot
) : ShellRepository {
constructor(context: Context, scope: CoroutineScope) : this(scope, nativeLoader(context))
constructor(context: Context, scope: CoroutineScope, directories: () -> PrivateDirectories) :
this(scope, nativeLoader(context, directories))
private val mutableSnapshot = MutableStateFlow(ShellSnapshot(ShellState.Loading, emptyList()))
override val snapshot: StateFlow<ShellSnapshot> = mutableSnapshot
private val revision = AtomicLong()
@@ -46,17 +52,52 @@ class NativeFixtureShellRepository internal constructor(
ShellSnapshot(ShellState.Error, emptyList(), nativeFailure = error.kind.toFacade())
}
if (revision.get() == operation) {
mutableSnapshot.update { result.copy(selectedTab = it.selectedTab) }
mutableSnapshot.update {
result.copy(
selectedTab = if (it.pages.isEmpty()) result.selectedTab else it.selectedTab
)
}
}
}
}
}
private fun nativeLoader(context: Context): suspend (ShellState) -> ShellSnapshot {
private fun nativeLoader(
context: Context,
directoryProvider: () -> PrivateDirectories
): suspend (ShellState) -> ShellSnapshot {
val runtime = NativeRuntime(context)
var storage: MobilePrivateRuntime? = null
var firstLoad = true
return { state ->
withContext(Dispatchers.IO) {
val shell = runtime.fixture(state.toNative())
var recovery: StorageRecovery? = null
val shell = if (firstLoad) {
firstLoad = false
try {
val directories = directoryProvider()
val initialized = storage ?: runtime.privateStorage(
MobilePrivateDirectories(
directories.files,
directories.noBackup,
directories.cache
)
)
storage = initialized
initialized.activate()
recovery = initialized.status().recovery?.toFacade()
initialized.shell()
} catch (error: MobilePrivateStorageFfiException.Failed) {
recovery = error.kind.toFacade()
mobilePrivateStorageFailure(error.kind)
} catch (_: java.io.IOException) {
recovery = StorageRecovery.InvalidRoots
runtime.fixture(MobileShellState.ERROR)
mobilePrivateStorageFailure(MobilePrivateStorageErrorKind.INVALID_ROOTS)
}
} else {
runtime.fixture(state.toNative())
}
val pages = shell.pages.map { page ->
ShellPage(
page.tab.toFacade(),
@@ -68,11 +109,39 @@ private fun nativeLoader(context: Context): suspend (ShellState) -> ShellSnapsho
}
val actual = shell.pages.firstOrNull()?.state
?: throw NativeBridgeException(NativeFailureKind.RustFailure)
ShellSnapshot(actual.toFacade(), pages)
ShellSnapshot(
actual.toFacade(),
pages,
selectedTab = shell.selectedTab.toFacade(),
storageRecovery = recovery
)
}
}
}
private fun MobilePrivateStorageErrorKind.toFacade(): StorageRecovery = when (this) {
MobilePrivateStorageErrorKind.INVALID_ROOTS -> StorageRecovery.InvalidRoots
MobilePrivateStorageErrorKind.UNSAFE_PATH -> StorageRecovery.UnsafePath
MobilePrivateStorageErrorKind.MISSING_ROOT -> StorageRecovery.MissingRoot
MobilePrivateStorageErrorKind.ACCESS_DENIED -> StorageRecovery.AccessDenied
MobilePrivateStorageErrorKind.READ_ONLY -> StorageRecovery.ReadOnly
MobilePrivateStorageErrorKind.STORAGE_FULL -> StorageRecovery.StorageFull
MobilePrivateStorageErrorKind.BUSY -> StorageRecovery.Busy
MobilePrivateStorageErrorKind.IO -> StorageRecovery.Io
MobilePrivateStorageErrorKind.CORRUPT_LAYOUT -> StorageRecovery.CorruptLayout
MobilePrivateStorageErrorKind.MIGRATION_REQUIRED -> StorageRecovery.MigrationRequired
MobilePrivateStorageErrorKind.CORRUPT_CONFIGURATION -> StorageRecovery.CorruptConfiguration
MobilePrivateStorageErrorKind.CONFIGURATION_MISSING_WITH_DATA ->
StorageRecovery.ConfigurationMissingWithData
MobilePrivateStorageErrorKind.INTERRUPTED_SETUP -> StorageRecovery.InterruptedSetup
MobilePrivateStorageErrorKind.RESTORED_WITHOUT_KEYSTORE ->
StorageRecovery.RestoredWithoutKeystore
MobilePrivateStorageErrorKind.SECURE_STORAGE_LOCKED -> StorageRecovery.SecureStorageLocked
MobilePrivateStorageErrorKind.SECURE_STORAGE_UNAVAILABLE ->
StorageRecovery.SecureStorageUnavailable
MobilePrivateStorageErrorKind.INVALID_SECURE_BINDING -> StorageRecovery.InvalidSecureBinding
}
private fun MobileShellState.toFacade(): ShellState = when (this) {
MobileShellState.LOADING -> ShellState.Loading
MobileShellState.EMPTY -> ShellState.Empty
@@ -0,0 +1,25 @@
package de.rfc1437.ironstorage.core
data class PrivateDirectories(val files: String, val noBackup: String, val cache: String) {
override fun toString(): String = "PrivateDirectories([REDACTED])"
}
enum class StorageRecovery {
InvalidRoots,
UnsafePath,
MissingRoot,
AccessDenied,
ReadOnly,
StorageFull,
Busy,
Io,
CorruptLayout,
MigrationRequired,
CorruptConfiguration,
ConfigurationMissingWithData,
InterruptedSetup,
RestoredWithoutKeystore,
SecureStorageLocked,
SecureStorageUnavailable,
InvalidSecureBinding
}
@@ -44,7 +44,8 @@ data class ShellSnapshot(
val state: ShellState,
val pages: List<ShellPage>,
val selectedTab: ShellTab = ShellTab.Home,
val nativeFailure: NativeFailure? = null
val nativeFailure: NativeFailure? = null,
val storageRecovery: StorageRecovery? = null
)
enum class NativeFailure {