From e2c8abb337e1af6a94dc131da924fc6c7061003d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 30 Mar 2026 15:31:55 +0200 Subject: [PATCH 1/2] fix(oc-capability): parsing wcf entries Signed-off-by: alperozturk96 --- .../utils/OCCapabilityJsonToListTests.kt | 176 ++++++++++++++++++ .../extensions/OCCapabilityExtensions.kt | 19 +- 2 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt diff --git a/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt b/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt new file mode 100644 index 000000000000..7aaa1c5fa915 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt @@ -0,0 +1,176 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import com.nextcloud.utils.extensions.forbiddenFilenameBaseNames +import com.nextcloud.utils.extensions.forbiddenFilenameCharacters +import com.nextcloud.utils.extensions.forbiddenFilenameExtensions +import com.nextcloud.utils.extensions.forbiddenFilenames +import com.owncloud.android.AbstractIT +import com.owncloud.android.lib.resources.status.OCCapability +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue +import org.junit.Test + +class OCCapabilityJsonToListTests : AbstractIT() { + private var capability: OCCapability = fileDataStorageManager.getCapability(account.name) + + // region Valid Input + @Test + fun testForbiddenFilenamesParsedCorrectly() { + capability.forbiddenFilenamesJson = """[".htaccess", ".htaccess"]""" + val result = capability.forbiddenFilenames() + assertEquals(listOf(".htaccess", ".htaccess"), result) + } + + @Test + fun testForbiddenFilenameBaseNamesParsedCorrectly() { + capability.forbiddenFilenameBaseNamesJson = """["con", "prn", "aux"]""" + val result = capability.forbiddenFilenameBaseNames() + assertEquals(listOf("con", "prn", "aux"), result) + } + + @Test + fun testForbiddenFilenameExtensionsParsedCorrectly() { + capability.forbiddenFilenameExtensionJson = """[" ",".",".part"]""" + val result = capability.forbiddenFilenameExtensions() + assertEquals(listOf(" ", ".", ".part"), result) + } + + @Test + fun testForbiddenFilenameCharactersParsedCorrectly() { + capability.forbiddenFilenameCharactersJson = """["<", ">", ":", "\\", "/", "|", "?", "*", "&"]""" + val result = capability.forbiddenFilenameCharacters() + assertEquals(listOf("<", ">", ":", "\\", "/", "|", "?", "*", "&"), result) + } + + @Test + fun testEmptyArrayReturnsEmptyList() { + capability.forbiddenFilenamesJson = """[]""" + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + + @Test + fun testSingleElementArray() { + capability.forbiddenFilenamesJson = """[".htaccess"]""" + val result = capability.forbiddenFilenames() + assertEquals(listOf(".htaccess"), result) + } + + @Test + fun testArrayWithWhitespaceAroundJson() { + capability.forbiddenFilenameBaseNamesJson = """ + ["con", "prn", "aux", "nul", "com0", "com1", "com2", "com3", "com4", + "com5", "com6", "com7", "com8", "com9", "com¹", "com²", "com³", + "lpt0", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", + "lpt8", "lpt9", "lpt¹", "lpt²", "lpt³"] + """ + val result = capability.forbiddenFilenameBaseNames() + assertEquals(30, result.size) + assertTrue(result.contains("con")) + assertTrue(result.contains("lpt³")) + } + + @Test + fun testUnicodeCharactersPreserved() { + capability.forbiddenFilenameBaseNamesJson = """["com¹", "com²", "com³", "lpt¹", "lpt²", "lpt³"]""" + val result = capability.forbiddenFilenameBaseNames() + assertEquals(listOf("com¹", "com²", "com³", "lpt¹", "lpt²", "lpt³"), result) + } + + @Test + fun testDuplicateEntriesPreserved() { + capability.forbiddenFilenameExtensionJson = """[".part", ".part"]""" + val result = capability.forbiddenFilenameExtensions() + assertEquals(listOf(".part", ".part"), result) + } + // endregion + + // region Null and Blank Input + @Test + fun testNullJsonReturnsEmptyList() { + capability.forbiddenFilenamesJson = null + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + + @Test + fun testBlankJsonReturnsEmptyList() { + capability.forbiddenFilenamesJson = " " + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + + @Test + fun testEmptyStringJsonReturnsEmptyList() { + capability.forbiddenFilenamesJson = "" + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + // endregion + + // region Malformed Input + @Test + fun testMalformedJsonReturnsEmptyList() { + capability.forbiddenFilenamesJson = """[".htaccess", """ + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + + @Test + fun testNonArrayJsonObjectReturnsEmptyList() { + capability.forbiddenFilenamesJson = """{"key": "value"}""" + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + + @Test + fun testPlainStringJsonReturnsEmptyList() { + capability.forbiddenFilenamesJson = """.htaccess""" + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + + @Test + fun testHtmlErrorPageReturnsEmptyList() { + capability.forbiddenFilenamesJson = "Internal Server Error" + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + + @Test + fun testJsonNullLiteralReturnsEmptyList() { + capability.forbiddenFilenamesJson = "null" + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + + // endregion + + // region Oversized Input + @Test + fun testOversizedJsonReturnsEmptyList() { + val hugeEntry = "a".repeat(1024) + val entries = Array(600) { """"$hugeEntry"""" } + capability.forbiddenFilenamesJson = "[${entries.joinToString(",")}]" + val result = capability.forbiddenFilenames() + assertEquals(emptyList(), result) + } + + @Test + fun testJsonJustUnderSizeLimitIsParsed() { + val entries = Array(100) { i -> """"entry$i"""" } + capability.forbiddenFilenamesJson = "[${entries.joinToString(",")}]" + val result = capability.forbiddenFilenames() + assertEquals(100, result.size) + assertEquals("entry0", result[0]) + assertEquals("entry99", result[99]) + } + // endregion +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.kt index 8628548766c1..55483af5715b 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.kt @@ -8,12 +8,16 @@ package com.nextcloud.utils.extensions import com.google.gson.Gson +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.status.NextcloudVersion import com.owncloud.android.lib.resources.status.OCCapability import org.json.JSONException private val gson = Gson() +private const val TAG = "OCCapabilityExtensions" +private const val MAX_JSON_BYTES = 512 * 1024 + /** * Determines whether **Windows-compatible file (WCF)** restrictions should be applied * for the current server version and configuration. @@ -47,13 +51,18 @@ fun OCCapability.shouldRemoveNonPrintableUnicodeCharactersAndConvertToUTF8(): Bo forbiddenFilenameExtensions().isNotEmpty() || forbiddenFilenameBaseNames().isNotEmpty() -@Suppress("ReturnCount") -private fun jsonToList(json: String?): List { - if (json == null) return emptyList() +@Suppress("ReturnCount", "TooGenericExceptionCaught") +fun jsonToList(json: String?): List { + if (json.isNullOrBlank()) return emptyList() + + if (json.length > MAX_JSON_BYTES) { + Log_OC.e(TAG, "jsonToList: JSON exceeds size limit (${json.length} chars), skipping") + return emptyList() + } return try { - return gson.fromJson(json, Array::class.java).toList() - } catch (_: JSONException) { + gson.fromJson(json, Array::class.java)?.toList() ?: emptyList() + } catch (_: Throwable) { emptyList() } } From 0a881d93a33427e6a7e46641b797bf495d734689 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 30 Mar 2026 16:08:06 +0200 Subject: [PATCH 2/2] fix codacy Signed-off-by: alperozturk96 --- .../java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt b/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt index 7aaa1c5fa915..28a6660dac88 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/OCCapabilityJsonToListTests.kt @@ -17,6 +17,7 @@ import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue import org.junit.Test +@Suppress("MagicNumber", "TooManyFunctions") class OCCapabilityJsonToListTests : AbstractIT() { private var capability: OCCapability = fileDataStorageManager.getCapability(account.name)