Skip to content

Commit c0b0be1

Browse files
Merge pull request #14834 from nextcloud/warn-user-for-invalid-filename
Warn Users About Invalid Filenames During Download
2 parents 5682154 + 7c0a4c6 commit c0b0be1

File tree

3 files changed

+76
-3
lines changed

3 files changed

+76
-3
lines changed

app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java

+13-3
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212
package com.owncloud.android.operations;
1313

1414
import android.content.Context;
15+
import android.os.Handler;
16+
import android.os.Looper;
1517
import android.text.TextUtils;
1618
import android.webkit.MimeTypeMap;
1719

1820
import com.nextcloud.client.account.User;
21+
import com.nextcloud.utils.extensions.ContextExtensionsKt;
22+
import com.owncloud.android.R;
1923
import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
2024
import com.owncloud.android.datamodel.FileDataStorageManager;
2125
import com.owncloud.android.datamodel.OCFile;
@@ -62,8 +66,8 @@ public class DownloadFileOperation extends RemoteOperation {
6266
private Set<OnDatatransferProgressListener> dataTransferListeners = new HashSet<>();
6367
private long modificationTimestamp;
6468
private DownloadFileRemoteOperation downloadOperation;
65-
6669
private final AtomicBoolean cancellationRequested = new AtomicBoolean(false);
70+
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
6771

6872
public DownloadFileOperation(User user,
6973
OCFile file,
@@ -162,13 +166,19 @@ protected RemoteOperationResult run(OwnCloudClient client) {
162166
/// perform the download
163167
synchronized(cancellationRequested) {
164168
if (cancellationRequested.get()) {
165-
return new RemoteOperationResult(new OperationCancelledException());
169+
return new RemoteOperationResult<>(new OperationCancelledException());
166170
}
167171
}
168172

173+
final var isValidExtFilename = FileStorageUtils.isValidExtFilename(file.getFileName());
174+
if (!isValidExtFilename) {
175+
mainThreadHandler.post(() -> ContextExtensionsKt.showToast(context.get(), R.string.download_download_invalid_local_file_name));
176+
return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.INVALID_CHARACTER_IN_NAME);
177+
}
178+
169179
Context operationContext = context.get();
170180
if (operationContext == null) {
171-
return new RemoteOperationResult(RemoteOperationResult.ResultCode.UNKNOWN_ERROR);
181+
return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.UNKNOWN_ERROR);
172182
}
173183

174184
RemoteOperationResult result;

app/src/main/java/com/owncloud/android/utils/FileStorageUtils.java

+31
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,37 @@ private FileStorageUtils() {
6969
// utility class -> private constructor
7070
}
7171

72+
public static boolean isValidExtFilename(String name) {
73+
for (int i = 0; i < name.length(); i++) {
74+
char c = name.charAt(i);
75+
if (!isValidExtFilenameChar(c)) {
76+
return false;
77+
}
78+
}
79+
return true;
80+
}
81+
82+
/**
83+
* Checks whether the given character is valid in an extended file name.
84+
* <p>
85+
* Reference: <a href="https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/os/FileUtils.java;l=997">
86+
* android.os.FileUtils#isValidExtFilenameChar(char)
87+
* </a> from the Android Open Source Project.
88+
*
89+
* @param c the character to validate
90+
* @return true if the character is valid in a filename, false otherwise
91+
*/
92+
private static boolean isValidExtFilenameChar(char c) {
93+
if ((int) c <= 0x1F) {
94+
return false;
95+
}
96+
97+
return switch (c) {
98+
case '"', '*', ':', '/', '<', '>', '?', '\\', '|', 0x7F -> false;
99+
default -> true;
100+
};
101+
}
102+
72103
/**
73104
* Get local owncloud storage path for accountName.
74105
*/

app/src/test/java/com/nextcloud/client/utils/FileStorageUtilsTest.kt

+32
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,43 @@ package com.nextcloud.client.utils
1010
import com.nextcloud.client.preferences.SubFolderRule
1111
import com.owncloud.android.utils.FileStorageUtils
1212
import org.junit.Assert.assertEquals
13+
import org.junit.Assert.assertFalse
14+
import org.junit.Assert.assertTrue
1315
import org.junit.Test
1416
import java.io.File
1517
import java.util.Locale
1618

1719
class FileStorageUtilsTest {
20+
@Test
21+
fun testValidFilenames() {
22+
assertTrue(FileStorageUtils.isValidExtFilename("example.txt"))
23+
assertTrue(FileStorageUtils.isValidExtFilename("file_name-123"))
24+
assertTrue(FileStorageUtils.isValidExtFilename("normalFile"))
25+
}
26+
27+
@Test
28+
fun testInvalidFilenamesWithSpecialChars() {
29+
assertFalse(FileStorageUtils.isValidExtFilename("file:name.txt"))
30+
assertFalse(FileStorageUtils.isValidExtFilename("file*name"))
31+
assertFalse(FileStorageUtils.isValidExtFilename("file/name"))
32+
assertFalse(FileStorageUtils.isValidExtFilename("file\\name"))
33+
assertFalse(FileStorageUtils.isValidExtFilename("file|name"))
34+
assertFalse(FileStorageUtils.isValidExtFilename("file\"name"))
35+
assertFalse(FileStorageUtils.isValidExtFilename("file<name>"))
36+
assertFalse(FileStorageUtils.isValidExtFilename("file?name"))
37+
}
38+
39+
@Test
40+
fun testFilenamesWithControlCharacters() {
41+
assertFalse(FileStorageUtils.isValidExtFilename("file\u0001name"))
42+
assertFalse(FileStorageUtils.isValidExtFilename("file\u001Fname"))
43+
}
44+
45+
@Test
46+
fun testEmptyFilename() {
47+
assertTrue(FileStorageUtils.isValidExtFilename(""))
48+
}
49+
1850
@Test
1951
fun testInstantUploadPathSubfolder() {
2052
val file = File("/sdcard/DCIM/subfolder/file.jpg")

0 commit comments

Comments
 (0)