app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
Aapp/desktop/src/main/kotlin/org/radroots/studio/application/NativeCompatibility.kt | 32++++++++++++++++++++++++++++++++
Mapp/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt | 10+---------
Aapp/desktop/src/test/kotlin/org/radroots/studio/application/NativeCompatibilityTest.kt | 35+++++++++++++++++++++++++++++++++++
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, + ) +}