feat(platform): adopt hardened core contracts

This commit is contained in:
2026-07-10 12:25:17 +02:00
parent e46a2522ce
commit 2271f453f1
5 changed files with 64 additions and 14 deletions

View File

@@ -10,6 +10,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import uniffi.vnidrop.ReceiveOutputSink
import java.io.OutputStream
import java.util.UUID
@Composable
actual fun rememberFileSystemService(): FileSystemService {
@@ -59,10 +60,10 @@ private class AndroidFileSystemService(
if (!hasPermission) return FolderAccessStatus.PermissionRequired
return runCatching {
val probe = AndroidTreeReceiveOutputSink(context, uri)
val probeName = ".vnidrop-write-test"
val probeName = ".vnidrop-write-test-${UUID.randomUUID()}"
probe.startFile(probeName)
probe.writeChunk(probeName, byteArrayOf())
probe.finishFile(probeName)
probe.abortFile(probeName, "write probe complete")
FolderAccessStatus.Writable
}.getOrDefault(FolderAccessStatus.Unavailable)
}
@@ -72,26 +73,62 @@ private class AndroidTreeReceiveOutputSink(
private val context: Context,
private val treeUri: Uri,
) : ReceiveOutputSink {
private val streams = mutableMapOf<String, OutputStream>()
private data class PendingDocument(
val stream: OutputStream,
val temporaryUri: Uri,
val parentUri: Uri,
val finalName: String,
)
private val pending = mutableMapOf<String, PendingDocument>()
override fun startFile(relativePath: String) {
streams[relativePath]?.close()
val documentUri = createDocument(relativePath)
val stream = context.contentResolver.openOutputStream(documentUri, "w")
check(relativePath !in pending) { "Output stream is already open for $relativePath" }
val (parent, finalName) = resolveParent(relativePath)
check(findChild(parent, finalName) == null) { "Destination already exists: $relativePath" }
val temporaryName = ".$finalName.vnidrop-${UUID.randomUUID()}.part"
val temporaryUri = DocumentsContract.createDocument(
context.contentResolver,
parent,
"application/octet-stream",
temporaryName,
) ?: error("Could not create temporary file for $relativePath")
val stream = context.contentResolver.openOutputStream(temporaryUri, "w")
?: error("Could not open output stream for $relativePath")
streams[relativePath] = stream
pending[relativePath] = PendingDocument(stream, temporaryUri, parent, finalName)
}
override fun writeChunk(relativePath: String, bytes: ByteArray) {
val stream = streams[relativePath] ?: error("Output stream is not open for $relativePath")
stream.write(bytes)
val document = pending[relativePath] ?: error("Output stream is not open for $relativePath")
document.stream.write(bytes)
}
override fun finishFile(relativePath: String) {
streams.remove(relativePath)?.close()
val document = pending.remove(relativePath) ?: error("Output stream is not open for $relativePath")
try {
document.stream.close()
check(findChild(document.parentUri, document.finalName) == null) { "Destination already exists: $relativePath" }
checkNotNull(
DocumentsContract.renameDocument(
context.contentResolver,
document.temporaryUri,
document.finalName,
),
) { "Could not commit received file $relativePath" }
} catch (error: Throwable) {
runCatching { document.stream.close() }
DocumentsContract.deleteDocument(context.contentResolver, document.temporaryUri)
throw error
}
}
private fun createDocument(relativePath: String): Uri {
override fun abortFile(relativePath: String, reason: String) {
val document = pending.remove(relativePath) ?: return
runCatching { document.stream.close() }
DocumentsContract.deleteDocument(context.contentResolver, document.temporaryUri)
}
private fun resolveParent(relativePath: String): Pair<Uri, String> {
val parts = relativePath.split('/').filter { it.isNotBlank() }
require(parts.isNotEmpty()) { "relative path must not be empty" }
var parent = DocumentsContract.buildDocumentUriUsingTree(
@@ -103,8 +140,7 @@ private class AndroidTreeReceiveOutputSink(
?: DocumentsContract.createDocument(context.contentResolver, parent, DocumentsContract.Document.MIME_TYPE_DIR, name)
?: error("Could not create directory $name")
}
return DocumentsContract.createDocument(context.contentResolver, parent, "application/octet-stream", parts.last())
?: error("Could not create file ${parts.last()}")
return parent to parts.last()
}
private fun findChild(parent: Uri, name: String): Uri? {