mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-12 03:09:15 +00:00
multiplatform: bound shared file URI copies (#7075)
* Bound shared file URI copies * test: cover multi-read path in bounded URI file copy * android, desktop: bound URI copies by sender badge file limit * android, desktop: show file-too-large alert on bounded URI copy overrun --------- Co-authored-by: Paul Bottinelli <paul.bottinelli@trailofbits.com>
This commit is contained in:
+2
-2
@@ -838,7 +838,7 @@ fun ComposeView(
|
||||
if (remoteHost == null) saveAnimImage(it.uri)
|
||||
else CryptoFile.desktopPlain(it.uri)
|
||||
is UploadContent.Video ->
|
||||
if (remoteHost == null) saveFileFromUri(it.uri, hiddenFileNamePrefix = "video")
|
||||
if (remoteHost == null) saveFileFromUri(it.uri, cs.maxFileSize, hiddenFileNamePrefix = "video")
|
||||
else CryptoFile.desktopPlain(it.uri)
|
||||
}
|
||||
if (file != null) {
|
||||
@@ -887,7 +887,7 @@ fun ComposeView(
|
||||
}
|
||||
is ComposePreview.FilePreview -> {
|
||||
val file = if (remoteHost == null) {
|
||||
saveFileFromUri(preview.uri)
|
||||
saveFileFromUri(preview.uri, cs.maxFileSize)
|
||||
} else {
|
||||
CryptoFile.desktopPlain(preview.uri)
|
||||
}
|
||||
|
||||
+33
-2
@@ -256,6 +256,7 @@ expect suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean)
|
||||
|
||||
fun saveFileFromUri(
|
||||
uri: URI,
|
||||
maxBytes: Long,
|
||||
withAlertOnException: Boolean = true,
|
||||
hiddenFileNamePrefix: String? = null
|
||||
): CryptoFile? {
|
||||
@@ -277,7 +278,7 @@ fun saveFileFromUri(
|
||||
val destFile = File(getAppFilePath(destFileName))
|
||||
if (encrypted) {
|
||||
createTmpFileAndDelete { tmpFile ->
|
||||
Files.copy(inputStream, tmpFile.toPath())
|
||||
copyInputStreamToFile(inputStream, tmpFile, maxBytes)
|
||||
try {
|
||||
val args = encryptCryptoFile(tmpFile.absolutePath, destFile.absolutePath)
|
||||
CryptoFile(destFileName, args)
|
||||
@@ -288,7 +289,7 @@ fun saveFileFromUri(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Files.copy(inputStream, destFile.toPath())
|
||||
copyInputStreamToFile(inputStream, destFile, maxBytes)
|
||||
CryptoFile.plain(destFileName)
|
||||
}
|
||||
} else {
|
||||
@@ -297,6 +298,15 @@ fun saveFileFromUri(
|
||||
|
||||
null
|
||||
}
|
||||
} catch (e: FileTooLargeException) {
|
||||
Log.e(TAG, "Util.kt saveFileFromUri file too large: ${e.message}")
|
||||
if (withAlertOnException) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.large_file),
|
||||
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxBytes))
|
||||
)
|
||||
}
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Util.kt saveFileFromUri error: ${e.stackTraceToString()}")
|
||||
if (withAlertOnException) showWrongUriAlert()
|
||||
@@ -305,6 +315,27 @@ fun saveFileFromUri(
|
||||
}
|
||||
}
|
||||
|
||||
class FileTooLargeException(maxBytes: Long) : IOException("file exceeds $maxBytes bytes")
|
||||
|
||||
fun copyInputStreamToFile(inputStream: InputStream, destFile: File, maxBytes: Long) {
|
||||
try {
|
||||
destFile.outputStream().use { output ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var copied = 0L
|
||||
while (true) {
|
||||
val read = inputStream.read(buffer)
|
||||
if (read < 0) break
|
||||
if (copied > maxBytes - read) throw FileTooLargeException(maxBytes)
|
||||
output.write(buffer, 0, read)
|
||||
copied += read
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
destFile.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun saveWallpaperFile(uri: URI): String? {
|
||||
val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath("")))
|
||||
val destFile = File(getWallpaperFilePath(destFileName))
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.views.helpers.FileTooLargeException
|
||||
import chat.simplex.common.views.helpers.copyInputStreamToFile
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import kotlin.io.path.createTempFile
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertContentEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class UtilsFileCopyTest {
|
||||
@Test
|
||||
fun testCopyInputStreamAllowsLimitBoundary() {
|
||||
val dest = createTempFile().toFile()
|
||||
val data = ByteArray(4) { it.toByte() }
|
||||
|
||||
copyInputStreamToFile(ByteArrayInputStream(data), dest, maxBytes = 4)
|
||||
|
||||
assertTrue(dest.exists())
|
||||
assertContentEquals(data, dest.readBytes())
|
||||
dest.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCopyInputStreamRejectsAndDeletesOversizedOutput() {
|
||||
val dest = createTempFile().toFile()
|
||||
val data = ByteArray(5) { it.toByte() }
|
||||
|
||||
assertFailsWith<FileTooLargeException> {
|
||||
copyInputStreamToFile(ByteArrayInputStream(data), dest, maxBytes = 4)
|
||||
}
|
||||
|
||||
assertFalse(dest.exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCopyInputStreamCopiesAcrossMultipleReads() {
|
||||
val dest = createTempFile().toFile()
|
||||
val data = ByteArray(20) { it.toByte() }
|
||||
|
||||
// Delivered 3 bytes per read, so the copy loop runs many iterations and accumulates copied > 0
|
||||
copyInputStreamToFile(chunkedStream(data, 3), dest, maxBytes = 20)
|
||||
|
||||
assertTrue(dest.exists())
|
||||
assertContentEquals(data, dest.readBytes())
|
||||
dest.delete()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCopyInputStreamRejectsAndDeletesWhenLimitCrossedMidStream() {
|
||||
val dest = createTempFile().toFile()
|
||||
val data = ByteArray(10) { it.toByte() }
|
||||
|
||||
// With 3-byte reads and a 7-byte limit, 6 bytes are written before the next read would exceed it
|
||||
assertFailsWith<FileTooLargeException> {
|
||||
copyInputStreamToFile(chunkedStream(data, 3), dest, maxBytes = 7)
|
||||
}
|
||||
|
||||
assertFalse(dest.exists())
|
||||
}
|
||||
|
||||
// Returns at most chunkSize bytes per read to force the copy loop to iterate, regardless of buffer size
|
||||
private fun chunkedStream(data: ByteArray, chunkSize: Int): InputStream =
|
||||
object : ByteArrayInputStream(data) {
|
||||
override fun read(b: ByteArray, off: Int, len: Int): Int = super.read(b, off, minOf(len, chunkSize))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user