mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
fix(receive): harden invitation open path and cover receive loop
Tighten cold-open and in-app invitation handling so ticket acquisition is stricter and less racey across hosts, and expand automated coverage for the receive history and acquisition flow before merge.
This commit is contained in:
@@ -40,6 +40,7 @@ import com.vnidrop.app.ui.theme.VniDropTheme
|
||||
import com.vnidrop.app.ui.theme.rememberResolvedDarkTheme
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
@Composable
|
||||
fun App(dependencies: AppDependencies) {
|
||||
@@ -82,7 +83,20 @@ fun App(dependencies: AppDependencies) {
|
||||
dependencies.externalInvitations.invitations.collect { invitation ->
|
||||
appViewModel.selectDestination(AppDestination.Receive)
|
||||
if (invitation.isSuccess) {
|
||||
receiveViewModel.coreState.filter { it.isInitialized }.first()
|
||||
// Cold-open can race app startup. Wait for core before inspecting so
|
||||
// the ticket is not dropped as "not initialized", but do not block
|
||||
// forever if initialization failed.
|
||||
val ready = withTimeoutOrNull(30_000) {
|
||||
receiveViewModel.coreState.filter { it.isInitialized }.first()
|
||||
}
|
||||
if (ready == null) {
|
||||
receiveViewModel.onInvitationResult(
|
||||
ReceiveMethod.InvitationFile,
|
||||
Result.failure(IllegalStateException("VniDrop is still starting up. Open the invitation again in a moment.")),
|
||||
)
|
||||
return@collect
|
||||
}
|
||||
// Avoid clobbering an in-flight inspection or receive.
|
||||
receiveViewModel.state.filter { state ->
|
||||
!state.isInspecting && !state.isReceiving && state.ticket.isBlank()
|
||||
}.first()
|
||||
|
||||
@@ -34,3 +34,19 @@ internal fun validateInvitation(raw: String): Result<String> = runCatching {
|
||||
require(raw.encodeToByteArray().size <= MaxVniDropInvitationBytes) { "The invitation is too large" }
|
||||
raw
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode invitation document bytes as strict UTF-8 text.
|
||||
*
|
||||
* Hosts often receive invitation files as opaque binary streams. Reject payloads
|
||||
* that are not valid UTF-8 so binary junk never reaches ticket inspection.
|
||||
*/
|
||||
fun decodeInvitationBytes(bytes: ByteArray): String {
|
||||
require(bytes.isNotEmpty()) { "The invitation is empty" }
|
||||
require(bytes.size <= MaxVniDropInvitationBytes) { "The invitation is too large" }
|
||||
val text = bytes.decodeToString()
|
||||
// decodeToString() replaces malformed sequences; require a lossless round-trip.
|
||||
require(text.encodeToByteArray().contentEquals(bytes)) { "The invitation is not valid text" }
|
||||
require(text.isNotBlank()) { "The invitation is empty" }
|
||||
return text
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user