Send a concrete mime type for media shared with a wildcard one

Some apps share an image with the mime type image/* rather than a
concrete one. That value was carried straight into the uploaded event, so
the message reached the room with an unusable media type and bridges
rejected it as unsupported.

Resolve a wildcard subtype to a default for its top level type before
processing the upload, reusing the mapping the download path already
applies so the SDK can pick the right extension.
This commit is contained in:
hayaksi1
2026-08-17 04:01:05 +03:00
parent 23b75c3423
commit 4c5baf8d60
5 changed files with 76 additions and 18 deletions
@@ -63,4 +63,17 @@ object MimeTypes {
val subType = components.last() val subType = components.last()
return subType.isNotBlank() && subType != "*" return subType.isNotBlank() && subType != "*"
} }
/**
* A mime type with a wildcard subtype is a filter, not a media type, so replace it with a
* concrete default for its top level type.
*/
fun String?.withDefaultSubtype(): String = when {
this == null -> OctetStream
hasSubtype(this) -> this
this == Images -> Jpeg
this == Videos -> Mp4
this == Audio -> Mp3
else -> OctetStream
}
} }
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.core.mimetype
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.mimetype.MimeTypes.withDefaultSubtype
import org.junit.Test
class MimeTypesTest {
@Test
fun `withDefaultSubtype keeps a mime type that already has a subtype`() {
assertThat(MimeTypes.Png.withDefaultSubtype()).isEqualTo(MimeTypes.Png)
assertThat(MimeTypes.Mp4.withDefaultSubtype()).isEqualTo(MimeTypes.Mp4)
assertThat(MimeTypes.Pdf.withDefaultSubtype()).isEqualTo(MimeTypes.Pdf)
}
@Test
fun `withDefaultSubtype replaces a wildcard subtype with a concrete one`() {
assertThat(MimeTypes.Images.withDefaultSubtype()).isEqualTo(MimeTypes.Jpeg)
assertThat(MimeTypes.Videos.withDefaultSubtype()).isEqualTo(MimeTypes.Mp4)
assertThat(MimeTypes.Audio.withDefaultSubtype()).isEqualTo(MimeTypes.Mp3)
}
@Test
fun `withDefaultSubtype falls back to octet stream`() {
assertThat(MimeTypes.Any.withDefaultSubtype()).isEqualTo(MimeTypes.OctetStream)
assertThat("text/*".withDefaultSubtype()).isEqualTo(MimeTypes.OctetStream)
assertThat(null.withDefaultSubtype()).isEqualTo(MimeTypes.OctetStream)
}
}
@@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.impl.media
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.withDefaultSubtype
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.media.MediaSource
@@ -71,15 +71,8 @@ class RustMediaLoader(
val mediaFile = innerClient.getMediaFile( val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource, mediaSource = mediaSource,
filename = filename, filename = filename,
mimeType = when { // Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension.
mimeType == null -> MimeTypes.OctetStream mimeType = mimeType.withDefaultSubtype(),
MimeTypes.hasSubtype(mimeType) -> mimeType
// Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension.
mimeType == MimeTypes.Images -> MimeTypes.Jpeg
mimeType == MimeTypes.Videos -> MimeTypes.Mp4
mimeType == MimeTypes.Audio -> MimeTypes.Mp3
else -> MimeTypes.OctetStream
},
useCache = useCache, useCache = useCache,
tempDir = cacheDirectory.path, tempDir = cacheDirectory.path,
) )
@@ -29,6 +29,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
import io.element.android.libraries.core.mimetype.MimeTypes.withDefaultSubtype
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.FileInfo
@@ -82,15 +83,16 @@ class AndroidMediaPreProcessor(
mediaOptimizationConfig: MediaOptimizationConfig, mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo> = withContext(coroutineDispatchers.computation) { ): Result<MediaUploadInfo> = withContext(coroutineDispatchers.computation) {
runCatchingExceptions { runCatchingExceptions {
val resolvedMimeType = mimeType.withDefaultSubtype()
val result = when { val result = when {
mimeType == MimeTypes.Svg -> processSvgImage(uri, mimeType) resolvedMimeType == MimeTypes.Svg -> processSvgImage(uri, resolvedMimeType)
mimeType.isMimeTypeImage() -> { resolvedMimeType.isMimeTypeImage() -> {
val shouldBeCompressed = mediaOptimizationConfig.compressImages && mimeType !in notCompressibleImageTypes val shouldBeCompressed = mediaOptimizationConfig.compressImages && resolvedMimeType !in notCompressibleImageTypes
processImage(uri, mimeType, shouldBeCompressed) processImage(uri, resolvedMimeType, shouldBeCompressed)
} }
mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType, mediaOptimizationConfig.videoCompressionPreset) resolvedMimeType.isMimeTypeVideo() -> processVideo(uri, resolvedMimeType, mediaOptimizationConfig.videoCompressionPreset)
mimeType.isMimeTypeAudio() -> processAudio(uri, mimeType) resolvedMimeType.isMimeTypeAudio() -> processAudio(uri, resolvedMimeType)
else -> processFile(uri, mimeType) else -> processFile(uri, resolvedMimeType)
} }
if (deleteOriginal) { if (deleteOriginal) {
tryOrNull { tryOrNull {
@@ -65,9 +65,24 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
assertThat(height).isEqualTo(2268) assertThat(height).isEqualTo(2268)
} }
@Test
fun `test processing an image shared with a wildcard mime type`() = runTest {
val mediaUploadInfo = process(
asset = assetImageJpeg,
mimeType = MimeTypes.Images,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Jpeg)
}
private suspend fun TestScope.process( private suspend fun TestScope.process(
asset: Asset, asset: Asset,
mediaOptimizationConfig: MediaOptimizationConfig, mediaOptimizationConfig: MediaOptimizationConfig,
mimeType: String = asset.mimeType,
sdkIntVersion: Int = Build.VERSION_CODES.P, sdkIntVersion: Int = Build.VERSION_CODES.P,
deleteOriginal: Boolean = false, deleteOriginal: Boolean = false,
): MediaUploadInfo { ): MediaUploadInfo {
@@ -81,7 +96,7 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
val file = getFileFromAssets(context, asset.filename) val file = getFileFromAssets(context, asset.filename)
val result = sut.process( val result = sut.process(
uri = file.toUri(), uri = file.toUri(),
mimeType = asset.mimeType, mimeType = mimeType,
deleteOriginal = deleteOriginal, deleteOriginal = deleteOriginal,
mediaOptimizationConfig = mediaOptimizationConfig, mediaOptimizationConfig = mediaOptimizationConfig,
) )