commit 1b62f86d353263c39507d62d1c7954321c1ecb0f
parent 71a3695085b34ec97d41f2e461425bc2a3f36bba
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 23:40:56 +0000
application: verify native compatibility before open
- centralize the expected FFI and schema contract in Kotlin
- reject mismatched descriptors before requesting native open
- pass the verified expectation into storage-opening construction
- cover major hash and schema incompatibility paths
Diffstat:
3 files changed, 68 insertions(+), 9 deletions(-)
diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/application/NativeCompatibility.kt b/app/desktop/src/main/kotlin/org/radroots/studio/application/NativeCompatibility.kt
@@ -0,0 +1,32 @@
+package org.radroots.studio.application
+
+import org.radroots.studio.ffi.CompatibilityDescriptor
+import org.radroots.studio.ffi.CompatibilityExpectation
+
+internal const val EXPECTED_FFI_CONTRACT_HASH = "radroots-studio-native-v2-2026-08-03"
+internal val EXPECTED_FFI_CONTRACT_MAJOR: UShort = 2.toUShort()
+internal val MINIMUM_FFI_CONTRACT_MINOR: UShort = 0.toUShort()
+internal const val MINIMUM_STORAGE_SCHEMA: UInt = 5U
+internal const val MAXIMUM_STORAGE_SCHEMA: UInt = 9U
+
+internal class NativeCompatibilityException : IllegalStateException(
+ "The application and native runtime are incompatible.",
+)
+
+internal fun verifyNativeCompatibility(
+ descriptor: CompatibilityDescriptor,
+): CompatibilityExpectation {
+ val compatible = descriptor.contractMajor == EXPECTED_FFI_CONTRACT_MAJOR &&
+ descriptor.contractMinor >= MINIMUM_FFI_CONTRACT_MINOR &&
+ descriptor.contractHash == EXPECTED_FFI_CONTRACT_HASH &&
+ descriptor.currentSchemaVersion >= MINIMUM_STORAGE_SCHEMA &&
+ descriptor.minimumSchemaVersion <= MAXIMUM_STORAGE_SCHEMA
+ if (!compatible) throw NativeCompatibilityException()
+ return CompatibilityExpectation(
+ contractMajor = EXPECTED_FFI_CONTRACT_MAJOR,
+ minimumContractMinor = MINIMUM_FFI_CONTRACT_MINOR,
+ contractHash = EXPECTED_FFI_CONTRACT_HASH,
+ minimumSchemaVersion = MINIMUM_STORAGE_SCHEMA,
+ maximumSchemaVersion = MAXIMUM_STORAGE_SCHEMA,
+ )
+}
diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt b/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt
@@ -11,7 +11,6 @@ import org.radroots.studio.accounts.ui.StartupFailureScreen
import org.radroots.studio.accounts.ui.toUiModel
import org.radroots.studio.ffi.StudioAppCore
import org.radroots.studio.ffi.StudioException
-import org.radroots.studio.ffi.CompatibilityExpectation
import org.radroots.studio.ffi.compatibilityDescriptor
internal typealias StudioStoreFactory = (CoroutineScope) -> StudioAppStore
@@ -64,15 +63,8 @@ internal fun createStudioAppStore(scope: CoroutineScope): StudioAppStore {
val developmentMode = java.lang.Boolean.getBoolean("radroots.studio.development")
val descriptor = compatibilityDescriptor()
val core = StudioAppCore.openCompatible(
- expectation = CompatibilityExpectation(
- contractMajor = 2.toUShort(),
- minimumContractMinor = 0.toUShort(),
- contractHash = "radroots-studio-native-v2-2026-08-03",
- minimumSchemaVersion = 5U,
- maximumSchemaVersion = 9U,
- ),
+ expectation = verifyNativeCompatibility(descriptor),
developmentMode = developmentMode,
)
- check(descriptor.contractMajor == 2.toUShort())
return StudioAppStore(NativeStudioCoreGateway(core), scope)
}
diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/application/NativeCompatibilityTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/application/NativeCompatibilityTest.kt
@@ -0,0 +1,35 @@
+package org.radroots.studio.application
+
+import org.radroots.studio.ffi.CompatibilityDescriptor
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+
+class NativeCompatibilityTest {
+ @Test
+ fun acceptsOnlyTheDeclaredNativeContractAndSchemaWindow() {
+ val descriptor = compatibleDescriptor()
+ val expectation = verifyNativeCompatibility(descriptor)
+ assertEquals(EXPECTED_FFI_CONTRACT_MAJOR, expectation.contractMajor)
+ assertEquals(EXPECTED_FFI_CONTRACT_HASH, expectation.contractHash)
+
+ listOf(
+ descriptor.copy(contractMajor = 3.toUShort()),
+ descriptor.copy(contractHash = "wrong"),
+ descriptor.copy(currentSchemaVersion = 4U),
+ descriptor.copy(minimumSchemaVersion = 10U),
+ ).forEach { incompatible ->
+ assertFailsWith<NativeCompatibilityException> {
+ verifyNativeCompatibility(incompatible)
+ }
+ }
+ }
+
+ private fun compatibleDescriptor() = CompatibilityDescriptor(
+ contractMajor = EXPECTED_FFI_CONTRACT_MAJOR,
+ contractMinor = MINIMUM_FFI_CONTRACT_MINOR,
+ contractHash = EXPECTED_FFI_CONTRACT_HASH,
+ minimumSchemaVersion = MINIMUM_STORAGE_SCHEMA,
+ currentSchemaVersion = MAXIMUM_STORAGE_SCHEMA,
+ )
+}