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:
2026-07-12 04:31:19 +02:00
parent 996cc0a5ab
commit 4050a7c011
12 changed files with 301 additions and 30 deletions

View File

@@ -24,11 +24,26 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Preferred: exact VniDrop invitation MIME type. -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:mimeType="application/vnd.vnidrop.transfer"/>
</intent-filter>
<!-- Fallback: .vnd files often arrive as octet-stream / unknown MIME. -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="content"/>
<data android:scheme="file"/>
<data android:mimeType="*/*"/>
<data android:pathPattern=".*\\.vnd"/>
<data android:pathPattern=".*\\..*\\.vnd"/>
<data android:pathPattern=".*\\..*\\..*\\.vnd"/>
<data android:pathPattern=".*\\..*\\..*\\..*\\.vnd"/>
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"

View File

@@ -11,8 +11,7 @@ import com.vnidrop.app.feature.receive.ExternalInvitationController
import com.vnidrop.app.feature.receive.MaxVniDropInvitationBytes
import com.vnidrop.app.feature.receive.VniDropInvitationExtension
import com.vnidrop.app.feature.receive.VniDropInvitationMimeType
import java.nio.ByteBuffer
import java.nio.charset.CodingErrorAction
import com.vnidrop.app.feature.receive.decodeInvitationBytes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -53,15 +52,13 @@ class MainActivity : ComponentActivity() {
private fun readInvitation(uri: Uri, declaredType: String?): Result<String> = runCatching {
val resolvedType = declaredType ?: contentResolver.getType(uri)
val hasExpectedName = uri.lastPathSegment?.endsWith(".$VniDropInvitationExtension", ignoreCase = true) == true
val path = uri.path.orEmpty()
val lastSegment = uri.lastPathSegment.orEmpty()
val hasExpectedName = lastSegment.endsWith(".$VniDropInvitationExtension", ignoreCase = true) ||
path.endsWith(".$VniDropInvitationExtension", ignoreCase = true)
require(resolvedType == VniDropInvitationMimeType || hasExpectedName) { "This is not a VniDrop invitation" }
val bytes = contentResolver.openInputStream(uri)?.use { it.readNBytes(MaxVniDropInvitationBytes + 1) }
?: error("The invitation could not be opened")
require(bytes.size <= MaxVniDropInvitationBytes) { "The invitation is too large" }
Charsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes))
.toString()
decodeInvitationBytes(bytes)
}
}