Skip to content

Commit 93962d0

Browse files
authored
Merge pull request #8925 from christianrowlands/bugfix/cmr/extended-character-filename
#6449 Extended file name support to include characters from multiple languages, including Cyrillic and Han scripts
2 parents ea170fc + a608bff commit 93962d0

File tree

4 files changed

+170
-24
lines changed

4 files changed

+170
-24
lines changed

changelog.d/6449.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Extended file name support to include characters from multiple languages, including Cyrillic and Han scripts. ([#6449](https://github.com/element-hq/element-android/issues/6449))
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright (c) 2024 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.util
18+
19+
import androidx.test.ext.junit.runners.AndroidJUnit4
20+
import org.junit.Assert.assertEquals
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import org.matrix.android.sdk.InstrumentedTest
24+
import org.matrix.android.sdk.internal.session.DefaultFileService.Companion.DEFAULT_FILENAME
25+
import org.matrix.android.sdk.internal.util.file.safeFileName
26+
27+
/**
28+
* These tests are run on an Android device because they need to use the static
29+
* MimeTypeMap#getSingleton() method, which was failing in the unit test directory.
30+
*/
31+
@RunWith(AndroidJUnit4::class)
32+
class FileUtilTest : InstrumentedTest {
33+
34+
@Test
35+
fun shouldReturnOriginalFilenameWhenValidCharactersAreUsed() {
36+
val fileName = "validFileName.txt"
37+
val mimeType = "text/plain"
38+
val result = safeFileName(fileName, mimeType)
39+
assertEquals("validFileName.txt", result)
40+
}
41+
42+
@Test
43+
fun shouldReplaceInvalidCharactersWithUnderscores() {
44+
val fileName = "invalid/filename:with*chars?.txt"
45+
val mimeType = "text/plain"
46+
val result = safeFileName(fileName, mimeType)
47+
assertEquals("invalid/filename_with_chars_.txt", result)
48+
}
49+
50+
@Test
51+
fun shouldAllowCyrillicCharactersInTheFilename() {
52+
val fileName = "тестовыйФайл.txt"
53+
val mimeType = "text/plain"
54+
val result = safeFileName(fileName, mimeType)
55+
assertEquals("тестовыйФайл.txt", result)
56+
}
57+
58+
@Test
59+
fun shouldAllowHanCharactersInTheFilename() {
60+
val fileName = "测试文件.txt"
61+
val mimeType = "text/plain"
62+
val result = safeFileName(fileName, mimeType)
63+
assertEquals("测试文件.txt", result)
64+
}
65+
66+
@Test
67+
fun shouldReturnDefaultFilenameWhenInputIsNull() {
68+
val fileName = null
69+
val mimeType = "text/plain"
70+
val result = safeFileName(fileName, mimeType)
71+
assertEquals("$DEFAULT_FILENAME.txt", result)
72+
}
73+
74+
@Test
75+
fun shouldAddTheCorrectExtensionWhenMissing() {
76+
val fileName = "myDocument"
77+
val mimeType = "application/pdf"
78+
val result = safeFileName(fileName, mimeType)
79+
assertEquals("myDocument.pdf", result)
80+
}
81+
82+
@Test
83+
fun shouldReplaceInvalidCharactersAndAddTheCorrectExtension() {
84+
val fileName = "my*docu/ment"
85+
val mimeType = "application/pdf"
86+
val result = safeFileName(fileName, mimeType)
87+
assertEquals("my_docu/ment.pdf", result)
88+
}
89+
90+
@Test
91+
fun shouldNotModifyTheExtensionIfItMatchesTheMimeType() {
92+
val fileName = "report.pdf"
93+
val mimeType = "application/pdf"
94+
val result = safeFileName(fileName, mimeType)
95+
assertEquals("report.pdf", result)
96+
}
97+
98+
@Test
99+
fun shouldReplaceSpacesWithUnderscores() {
100+
val fileName = "my report.doc"
101+
val mimeType = "application/msword"
102+
val result = safeFileName(fileName, mimeType)
103+
assertEquals("my_report.doc", result)
104+
}
105+
106+
@Test
107+
fun shouldAppendExtensionIfFileNameHasNoneAndMimeTypeIsValid() {
108+
val fileName = "newfile"
109+
val mimeType = "image/jpeg"
110+
val result = safeFileName(fileName, mimeType)
111+
assertEquals("newfile.jpg", result)
112+
}
113+
114+
@Test
115+
fun shouldKeepHyphenatedNamesIntact() {
116+
val fileName = "my-file-name"
117+
val mimeType = "application/octet-stream"
118+
val result = safeFileName(fileName, mimeType)
119+
assertEquals("my-file-name.bin", result)
120+
}
121+
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session
1818

1919
import android.content.Context
2020
import android.net.Uri
21-
import android.webkit.MimeTypeMap
2221
import androidx.core.content.FileProvider
2322
import kotlinx.coroutines.CompletableDeferred
2423
import kotlinx.coroutines.completeWith
@@ -41,6 +40,7 @@ import org.matrix.android.sdk.internal.network.httpclient.addAuthenticationHeade
4140
import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
4241
import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
4342
import org.matrix.android.sdk.internal.util.file.AtomicFileCreator
43+
import org.matrix.android.sdk.internal.util.file.safeFileName
4444
import org.matrix.android.sdk.internal.util.time.Clock
4545
import org.matrix.android.sdk.internal.util.writeToFile
4646
import timber.log.Timber
@@ -247,28 +247,6 @@ internal class DefaultFileService @Inject constructor(
247247
}
248248
}
249249

250-
private fun safeFileName(fileName: String?, mimeType: String?): String {
251-
return buildString {
252-
// filename has to be safe for the Android System
253-
val result = fileName
254-
?.replace("[^a-z A-Z0-9\\\\.\\-]".toRegex(), "_")
255-
?.takeIf { it.isNotEmpty() }
256-
?: DEFAULT_FILENAME
257-
append(result)
258-
// Check that the extension is correct regarding the mimeType
259-
val extensionFromMime = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
260-
if (extensionFromMime != null) {
261-
// Compare
262-
val fileExtension = result.substringAfterLast(delimiter = ".", missingDelimiterValue = "")
263-
if (fileExtension.isEmpty() || fileExtension != extensionFromMime) {
264-
// Missing extension, or diff in extension, add the one provided by the mimetype
265-
append(".")
266-
append(extensionFromMime)
267-
}
268-
}
269-
}
270-
}
271-
272250
override fun isFileInCache(
273251
mxcUrl: String?,
274252
fileName: String,
@@ -368,6 +346,6 @@ internal class DefaultFileService @Inject constructor(
368346
private const val ENCRYPTED_FILENAME = "encrypted.bin"
369347

370348
// The extension would be added from the mimetype
371-
private const val DEFAULT_FILENAME = "file"
349+
const val DEFAULT_FILENAME = "file"
372350
}
373351
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright (c) 2024 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.util.file
18+
19+
import android.webkit.MimeTypeMap
20+
import org.matrix.android.sdk.internal.session.DefaultFileService.Companion.DEFAULT_FILENAME
21+
22+
/**
23+
* Remove any characters from the file name that are not supported by the Android OS,
24+
* and update the file extension to match the mimeType.
25+
*/
26+
fun safeFileName(fileName: String?, mimeType: String?): String {
27+
return buildString {
28+
// filename has to be safe for the Android System
29+
val result = fileName
30+
?.replace("[\\\\?%*:|\"<>\\s]".toRegex(), "_")
31+
?.takeIf { it.isNotEmpty() }
32+
?: DEFAULT_FILENAME
33+
append(result)
34+
// Check that the extension is correct regarding the mimeType
35+
val extensionFromMime = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) }
36+
if (extensionFromMime != null) {
37+
// Compare
38+
val fileExtension = result.substringAfterLast(delimiter = ".", missingDelimiterValue = "")
39+
if (fileExtension.isEmpty() || fileExtension != extensionFromMime) {
40+
// Missing extension, or diff in extension, add the one provided by the mimetype
41+
append(".")
42+
append(extensionFromMime)
43+
}
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)