Skip to content

Commit 1a88926

Browse files
Merge pull request #16780 from nextcloud/fix/handle-edge-cases-for-parsing-wcf-related-entries
fix(oc-capability): parsing wcf entries
2 parents 198f3b8 + 0a881d9 commit 1a88926

2 files changed

Lines changed: 191 additions & 5 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Nextcloud - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
package com.nextcloud.utils
9+
10+
import com.nextcloud.utils.extensions.forbiddenFilenameBaseNames
11+
import com.nextcloud.utils.extensions.forbiddenFilenameCharacters
12+
import com.nextcloud.utils.extensions.forbiddenFilenameExtensions
13+
import com.nextcloud.utils.extensions.forbiddenFilenames
14+
import com.owncloud.android.AbstractIT
15+
import com.owncloud.android.lib.resources.status.OCCapability
16+
import junit.framework.TestCase.assertEquals
17+
import junit.framework.TestCase.assertTrue
18+
import org.junit.Test
19+
20+
@Suppress("MagicNumber", "TooManyFunctions")
21+
class OCCapabilityJsonToListTests : AbstractIT() {
22+
private var capability: OCCapability = fileDataStorageManager.getCapability(account.name)
23+
24+
// region Valid Input
25+
@Test
26+
fun testForbiddenFilenamesParsedCorrectly() {
27+
capability.forbiddenFilenamesJson = """[".htaccess", ".htaccess"]"""
28+
val result = capability.forbiddenFilenames()
29+
assertEquals(listOf(".htaccess", ".htaccess"), result)
30+
}
31+
32+
@Test
33+
fun testForbiddenFilenameBaseNamesParsedCorrectly() {
34+
capability.forbiddenFilenameBaseNamesJson = """["con", "prn", "aux"]"""
35+
val result = capability.forbiddenFilenameBaseNames()
36+
assertEquals(listOf("con", "prn", "aux"), result)
37+
}
38+
39+
@Test
40+
fun testForbiddenFilenameExtensionsParsedCorrectly() {
41+
capability.forbiddenFilenameExtensionJson = """[" ",".",".part"]"""
42+
val result = capability.forbiddenFilenameExtensions()
43+
assertEquals(listOf(" ", ".", ".part"), result)
44+
}
45+
46+
@Test
47+
fun testForbiddenFilenameCharactersParsedCorrectly() {
48+
capability.forbiddenFilenameCharactersJson = """["<", ">", ":", "\\", "/", "|", "?", "*", "&"]"""
49+
val result = capability.forbiddenFilenameCharacters()
50+
assertEquals(listOf("<", ">", ":", "\\", "/", "|", "?", "*", "&"), result)
51+
}
52+
53+
@Test
54+
fun testEmptyArrayReturnsEmptyList() {
55+
capability.forbiddenFilenamesJson = """[]"""
56+
val result = capability.forbiddenFilenames()
57+
assertEquals(emptyList<String>(), result)
58+
}
59+
60+
@Test
61+
fun testSingleElementArray() {
62+
capability.forbiddenFilenamesJson = """[".htaccess"]"""
63+
val result = capability.forbiddenFilenames()
64+
assertEquals(listOf(".htaccess"), result)
65+
}
66+
67+
@Test
68+
fun testArrayWithWhitespaceAroundJson() {
69+
capability.forbiddenFilenameBaseNamesJson = """
70+
["con", "prn", "aux", "nul", "com0", "com1", "com2", "com3", "com4",
71+
"com5", "com6", "com7", "com8", "com9", "com¹", "com²", "com³",
72+
"lpt0", "lpt1", "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7",
73+
"lpt8", "lpt9", "lpt¹", "lpt²", "lpt³"]
74+
"""
75+
val result = capability.forbiddenFilenameBaseNames()
76+
assertEquals(30, result.size)
77+
assertTrue(result.contains("con"))
78+
assertTrue(result.contains("lpt³"))
79+
}
80+
81+
@Test
82+
fun testUnicodeCharactersPreserved() {
83+
capability.forbiddenFilenameBaseNamesJson = """["com¹", "com²", "com³", "lpt¹", "lpt²", "lpt³"]"""
84+
val result = capability.forbiddenFilenameBaseNames()
85+
assertEquals(listOf("com¹", "com²", "com³", "lpt¹", "lpt²", "lpt³"), result)
86+
}
87+
88+
@Test
89+
fun testDuplicateEntriesPreserved() {
90+
capability.forbiddenFilenameExtensionJson = """[".part", ".part"]"""
91+
val result = capability.forbiddenFilenameExtensions()
92+
assertEquals(listOf(".part", ".part"), result)
93+
}
94+
// endregion
95+
96+
// region Null and Blank Input
97+
@Test
98+
fun testNullJsonReturnsEmptyList() {
99+
capability.forbiddenFilenamesJson = null
100+
val result = capability.forbiddenFilenames()
101+
assertEquals(emptyList<String>(), result)
102+
}
103+
104+
@Test
105+
fun testBlankJsonReturnsEmptyList() {
106+
capability.forbiddenFilenamesJson = " "
107+
val result = capability.forbiddenFilenames()
108+
assertEquals(emptyList<String>(), result)
109+
}
110+
111+
@Test
112+
fun testEmptyStringJsonReturnsEmptyList() {
113+
capability.forbiddenFilenamesJson = ""
114+
val result = capability.forbiddenFilenames()
115+
assertEquals(emptyList<String>(), result)
116+
}
117+
// endregion
118+
119+
// region Malformed Input
120+
@Test
121+
fun testMalformedJsonReturnsEmptyList() {
122+
capability.forbiddenFilenamesJson = """[".htaccess", """
123+
val result = capability.forbiddenFilenames()
124+
assertEquals(emptyList<String>(), result)
125+
}
126+
127+
@Test
128+
fun testNonArrayJsonObjectReturnsEmptyList() {
129+
capability.forbiddenFilenamesJson = """{"key": "value"}"""
130+
val result = capability.forbiddenFilenames()
131+
assertEquals(emptyList<String>(), result)
132+
}
133+
134+
@Test
135+
fun testPlainStringJsonReturnsEmptyList() {
136+
capability.forbiddenFilenamesJson = """.htaccess"""
137+
val result = capability.forbiddenFilenames()
138+
assertEquals(emptyList<String>(), result)
139+
}
140+
141+
@Test
142+
fun testHtmlErrorPageReturnsEmptyList() {
143+
capability.forbiddenFilenamesJson = "<html><body>Internal Server Error</body></html>"
144+
val result = capability.forbiddenFilenames()
145+
assertEquals(emptyList<String>(), result)
146+
}
147+
148+
@Test
149+
fun testJsonNullLiteralReturnsEmptyList() {
150+
capability.forbiddenFilenamesJson = "null"
151+
val result = capability.forbiddenFilenames()
152+
assertEquals(emptyList<String>(), result)
153+
}
154+
155+
// endregion
156+
157+
// region Oversized Input
158+
@Test
159+
fun testOversizedJsonReturnsEmptyList() {
160+
val hugeEntry = "a".repeat(1024)
161+
val entries = Array(600) { """"$hugeEntry"""" }
162+
capability.forbiddenFilenamesJson = "[${entries.joinToString(",")}]"
163+
val result = capability.forbiddenFilenames()
164+
assertEquals(emptyList<String>(), result)
165+
}
166+
167+
@Test
168+
fun testJsonJustUnderSizeLimitIsParsed() {
169+
val entries = Array(100) { i -> """"entry$i"""" }
170+
capability.forbiddenFilenamesJson = "[${entries.joinToString(",")}]"
171+
val result = capability.forbiddenFilenames()
172+
assertEquals(100, result.size)
173+
assertEquals("entry0", result[0])
174+
assertEquals("entry99", result[99])
175+
}
176+
// endregion
177+
}

app/src/main/java/com/nextcloud/utils/extensions/OCCapabilityExtensions.kt

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
package com.nextcloud.utils.extensions
99

1010
import com.google.gson.Gson
11+
import com.owncloud.android.lib.common.utils.Log_OC
1112
import com.owncloud.android.lib.resources.status.NextcloudVersion
1213
import com.owncloud.android.lib.resources.status.OCCapability
1314
import org.json.JSONException
1415

1516
private val gson = Gson()
1617

18+
private const val TAG = "OCCapabilityExtensions"
19+
private const val MAX_JSON_BYTES = 512 * 1024
20+
1721
/**
1822
* Determines whether **Windows-compatible file (WCF)** restrictions should be applied
1923
* for the current server version and configuration.
@@ -47,13 +51,18 @@ fun OCCapability.shouldRemoveNonPrintableUnicodeCharactersAndConvertToUTF8(): Bo
4751
forbiddenFilenameExtensions().isNotEmpty() ||
4852
forbiddenFilenameBaseNames().isNotEmpty()
4953

50-
@Suppress("ReturnCount")
51-
private fun jsonToList(json: String?): List<String> {
52-
if (json == null) return emptyList()
54+
@Suppress("ReturnCount", "TooGenericExceptionCaught")
55+
fun jsonToList(json: String?): List<String> {
56+
if (json.isNullOrBlank()) return emptyList()
57+
58+
if (json.length > MAX_JSON_BYTES) {
59+
Log_OC.e(TAG, "jsonToList: JSON exceeds size limit (${json.length} chars), skipping")
60+
return emptyList()
61+
}
5362

5463
return try {
55-
return gson.fromJson(json, Array<String>::class.java).toList()
56-
} catch (_: JSONException) {
64+
gson.fromJson(json, Array<String>::class.java)?.toList() ?: emptyList()
65+
} catch (_: Throwable) {
5766
emptyList()
5867
}
5968
}

0 commit comments

Comments
 (0)