From a7157a082d158e1d55c8012148606cd5986fcf0d Mon Sep 17 00:00:00 2001 From: Dmitry Vorobiev Date: Sun, 7 Jul 2024 11:40:51 +0300 Subject: [PATCH 1/2] KTOR-7190: Add support for encoded non-text queries --- ktor-http/common/src/io/ktor/http/Codecs.kt | 138 +++++++++ .../common/src/io/ktor/http/URLBuilder.kt | 20 +- ktor-http/common/src/io/ktor/http/Url.kt | 2 + .../ktor/http/UrlDecodedParametersBuilder.kt | 87 ------ .../ktor/http/UrlEncodedParametersBuilder.kt | 207 +++++++++++++ .../test/io/ktor/tests/http/CodecTest.kt | 25 ++ .../test/io/ktor/tests/http/URLBuilderTest.kt | 3 + .../http/UrlDecodedParametersBuilderTest.kt | 135 -------- .../http/UrlEncodedParametersBuilderTest.kt | 293 ++++++++++++++++++ 9 files changed, 684 insertions(+), 226 deletions(-) delete mode 100644 ktor-http/common/src/io/ktor/http/UrlDecodedParametersBuilder.kt create mode 100644 ktor-http/common/src/io/ktor/http/UrlEncodedParametersBuilder.kt delete mode 100644 ktor-http/common/test/io/ktor/tests/http/UrlDecodedParametersBuilderTest.kt create mode 100644 ktor-http/common/test/io/ktor/tests/http/UrlEncodedParametersBuilderTest.kt diff --git a/ktor-http/common/src/io/ktor/http/Codecs.kt b/ktor-http/common/src/io/ktor/http/Codecs.kt index f187cf5f231..7c3f9155056 100644 --- a/ktor-http/common/src/io/ktor/http/Codecs.kt +++ b/ktor-http/common/src/io/ktor/http/Codecs.kt @@ -40,6 +40,7 @@ internal val ATTRIBUTE_CHARACTERS: Set = URL_ALPHABET_CHARS + setOf( * Characters allowed in url according to https://tools.ietf.org/html/rfc3986#section-2.3 */ private val SPECIAL_SYMBOLS = listOf('-', '.', '_', '~').map { it.code.toByte() } +private val SPECIAL_SYMBOLS_CHARS = listOf('-', '.', '_', '~') /** * Encode url part as specified in @@ -132,6 +133,131 @@ public fun String.encodeURLParameter( } } +private enum class Decodability { + UNDECODABLE, + UTF8_STRING, + BINARY +} + +private fun String.isDecodable(plusIsSpace: Boolean = false): Decodability { + var remainingPercentChars = 0 + var expectedUtf8Bytes = 0 + var remainingUtf8Bytes = 0 + var percentByte = 0 + var unicodeCharCode = 0 + + var decodability = Decodability.UTF8_STRING + + for (char in this) { + if (char == '%') { + if (remainingPercentChars != 0) { + decodability = Decodability.UNDECODABLE // "%0%"-like pattern + break + } + remainingPercentChars = 2 + percentByte = 0 + } else if (char in URL_ALPHABET_CHARS) { + if (char in HEX_ALPHABET && remainingPercentChars > 0) { + percentByte = percentByte shl 4 + percentByte = percentByte or charToHexDigit(char) + remainingPercentChars-- + if (remainingPercentChars == 0) { + if (remainingUtf8Bytes == 0) { + // Start of UTF-8 sequence + remainingUtf8Bytes = when { + percentByte and 0x80 == 0x00 -> 0 + percentByte and 0xe0 == 0xc0 -> 1 + percentByte and 0xe0 == 0xe0 -> 2 + percentByte and 0xf8 == 0xf0 -> 3 + else -> { + decodability = Decodability.BINARY // invalid UTF-8 sequence + 0 + } + } + expectedUtf8Bytes = remainingUtf8Bytes + + unicodeCharCode = when (remainingUtf8Bytes) { + 0 -> percentByte + 1 -> percentByte and 0x1f + 2 -> percentByte and 0x0f + 3 -> percentByte and 0x07 + else -> { + decodability = Decodability.UNDECODABLE // unreachable + break + } + } + } else if (percentByte and 0xc0 == 0x80) { + // Continuation of UTF-8 sequence + remainingUtf8Bytes-- + unicodeCharCode = unicodeCharCode shl 6 + unicodeCharCode = unicodeCharCode or (percentByte and 0x3f) + if (remainingUtf8Bytes == 0) { + // Check forbidden code points + if (unicodeCharCode in 0xd800..0xdfff) return Decodability.BINARY // invalid UTF-8 sequence + if (unicodeCharCode >= 0x110000) return Decodability.BINARY // invalid UTF-8 sequence + + // Check overlong encoding + when { + expectedUtf8Bytes == 0 && unicodeCharCode in 0x00..0x7f -> Unit + expectedUtf8Bytes == 1 && unicodeCharCode in 0x80..0x07ff -> Unit + expectedUtf8Bytes == 2 && unicodeCharCode in 0x0800..0xffff -> Unit + expectedUtf8Bytes == 3 && unicodeCharCode in 0x010000..0x10ffff -> Unit + else -> decodability = Decodability.BINARY // invalid UTF-8 sequence + } + } + } else { + // Unexpected byte in the middle of UTF-8 sequence + decodability = Decodability.BINARY // invalid UTF-8 sequence + } + } + } else if (remainingUtf8Bytes != 0) { + decodability = Decodability.BINARY // invalid UTF-8 sequence + } else if (remainingPercentChars != 0) { + decodability = Decodability.UNDECODABLE // "%0x"-like pattern + break + } + } else if (char in SPECIAL_SYMBOLS_CHARS) { + if (remainingUtf8Bytes != 0) { + decodability = Decodability.BINARY // invalid UTF-8 sequence + } else if (remainingPercentChars != 0) { + decodability = Decodability.UNDECODABLE // "%0~"-like pattern + break + } + } else if (char == '+') { + if (!plusIsSpace || remainingPercentChars != 0) { + decodability = Decodability.UNDECODABLE // plus is threatened as invalid input symbol here or "%0+"-like pattern + break + } else if (remainingUtf8Bytes != 0) { + decodability = Decodability.BINARY // invalid UTF-8 sequence + } + } else { + decodability = Decodability.UNDECODABLE // invalid input symbol + break + } + } + + if (decodability == Decodability.UTF8_STRING && remainingUtf8Bytes != 0) { + decodability = Decodability.BINARY // incomplete UTF-8 sequence + } + return if (remainingPercentChars != 0) { + Decodability.UNDECODABLE // "%0"-like pattern + } else { + decodability + } +} + +internal fun String.isDecodableToUTF8String(plusIsSpace: Boolean = false): Boolean { + return isDecodable(plusIsSpace) == Decodability.UTF8_STRING +} + +internal fun String.checkDecodableToUTF8String(plusIsSpace: Boolean = false): Boolean { + return when (isDecodable(plusIsSpace)) { + Decodability.UTF8_STRING -> true + Decodability.BINARY -> false + Decodability.UNDECODABLE -> throw URLDecodeException("Invalid percent-encoding sequence") + } +} + internal fun String.percentEncode(allowedSet: Set): String { val encodedCount = count { it !in allowedSet } if (encodedCount == 0) return this @@ -186,6 +312,18 @@ public fun String.decodeURLPart( charset: Charset = Charsets.UTF_8 ): String = decodeScan(start, end, false, charset) +/** + * Decode [this] as query parameter key. + */ +public fun String.decodeURLParameter( + plusIsSpace: Boolean = false +): String = decodeScan(0, length, plusIsSpace, Charsets.UTF_8) + +/** + * Decode [this] as query parameter value. Plus character will be decoded to space. + */ +internal fun String.decodeURLParameterValue(): String = decodeURLParameter(plusIsSpace = true) + private fun String.decodeScan(start: Int, end: Int, plusIsSpace: Boolean, charset: Charset): String { for (index in start until end) { val ch = this[index] diff --git a/ktor-http/common/src/io/ktor/http/URLBuilder.kt b/ktor-http/common/src/io/ktor/http/URLBuilder.kt index 8c7b0b873b4..c6eddbd157a 100644 --- a/ktor-http/common/src/io/ktor/http/URLBuilder.kt +++ b/ktor-http/common/src/io/ktor/http/URLBuilder.kt @@ -4,6 +4,8 @@ package io.ktor.http +import io.ktor.util.* + /** * Select default port value from protocol. */ @@ -76,15 +78,24 @@ public class URLBuilder( encodedPathSegments = value.map { it.encodeURLPathPart() } } - public var encodedParameters: ParametersBuilder = encodeParameters(parameters) + public var encodedParameters: ParametersBuilder = recreateEncodedBuilder(encodeParameters(parameters)) set(value) { - field = value - parameters = UrlDecodedParametersBuilder(value) + field = recreateEncodedBuilder(value) } - public var parameters: ParametersBuilder = UrlDecodedParametersBuilder(encodedParameters) + public lateinit var parameters: ParametersBuilder private set + private lateinit var rawEncodedParameters: ParametersBuilder + + private fun recreateEncodedBuilder(additionalParameters: ParametersBuilder): ParametersBuilder { + parameters = ParametersBuilder() + rawEncodedParameters = ParametersBuilder() + return UrlEncodedParametersBuilder(rawEncodedParameters, parameters).also { + it.appendAll(additionalParameters) + } + } + /** * Build a URL string */ @@ -109,6 +120,7 @@ public class URLBuilder( specifiedPort = port, pathSegments = pathSegments, parameters = parameters.build(), + rawEncodedParameters = rawEncodedParameters.build(), fragment = fragment, user = user, password = password, diff --git a/ktor-http/common/src/io/ktor/http/Url.kt b/ktor-http/common/src/io/ktor/http/Url.kt index 33ed979a7f6..b19c00f4fb1 100644 --- a/ktor-http/common/src/io/ktor/http/Url.kt +++ b/ktor-http/common/src/io/ktor/http/Url.kt @@ -13,6 +13,7 @@ package io.ktor.http * @property specifiedPort port number that was specified to override protocol's default * @property encodedPath encoded path without query string * @property parameters URL query parameters + * @property rawEncodedParameters encoded URL query parameters which can't be decoded as strings * @property fragment URL fragment (anchor name) * @property user username part of URL * @property password password part of URL @@ -24,6 +25,7 @@ public class Url internal constructor( public val specifiedPort: Int, public val pathSegments: List, public val parameters: Parameters, + public val rawEncodedParameters: Parameters, public val fragment: String, public val user: String?, public val password: String?, diff --git a/ktor-http/common/src/io/ktor/http/UrlDecodedParametersBuilder.kt b/ktor-http/common/src/io/ktor/http/UrlDecodedParametersBuilder.kt deleted file mode 100644 index aa46de0d769..00000000000 --- a/ktor-http/common/src/io/ktor/http/UrlDecodedParametersBuilder.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package io.ktor.http - -import io.ktor.util.* - -internal class UrlDecodedParametersBuilder( - private val encodedParametersBuilder: ParametersBuilder -) : ParametersBuilder { - - override fun build(): Parameters = decodeParameters(encodedParametersBuilder) - - override val caseInsensitiveName: Boolean = encodedParametersBuilder.caseInsensitiveName - - override fun getAll(name: String): List? = encodedParametersBuilder.getAll(name.encodeURLParameter()) - ?.map { it.decodeURLQueryComponent(plusIsSpace = true) } - - override fun contains(name: String): Boolean = encodedParametersBuilder.contains(name.encodeURLParameter()) - - override fun contains(name: String, value: String): Boolean = - encodedParametersBuilder.contains(name.encodeURLParameter(), value.encodeURLParameterValue()) - - override fun names(): Set = - encodedParametersBuilder.names().map { it.decodeURLQueryComponent() }.toSet() - - override fun isEmpty(): Boolean = encodedParametersBuilder.isEmpty() - - override fun entries(): Set>> = decodeParameters(encodedParametersBuilder).entries() - - override fun set(name: String, value: String) = - encodedParametersBuilder.set(name.encodeURLParameter(), value.encodeURLParameterValue()) - - override fun get(name: String): String? = - encodedParametersBuilder[name.encodeURLParameter()]?.decodeURLQueryComponent(plusIsSpace = true) - - override fun append(name: String, value: String) = - encodedParametersBuilder.append(name.encodeURLParameter(), value.encodeURLParameterValue()) - - override fun appendAll(stringValues: StringValues) = encodedParametersBuilder.appendAllEncoded(stringValues) - - override fun appendAll(name: String, values: Iterable) = - encodedParametersBuilder.appendAll(name.encodeURLParameter(), values.map { it.encodeURLParameterValue() }) - - override fun appendMissing(stringValues: StringValues) = - encodedParametersBuilder.appendMissing(encodeParameters(stringValues).build()) - - override fun appendMissing(name: String, values: Iterable) = - encodedParametersBuilder.appendMissing(name.encodeURLParameter(), values.map { it.encodeURLParameterValue() }) - - override fun remove(name: String) = - encodedParametersBuilder.remove(name.encodeURLParameter()) - - override fun remove(name: String, value: String): Boolean = - encodedParametersBuilder.remove(name.encodeURLParameter(), value.encodeURLParameterValue()) - - override fun removeKeysWithNoEntries() = encodedParametersBuilder.removeKeysWithNoEntries() - - override fun clear() = encodedParametersBuilder.clear() -} - -internal fun decodeParameters(parameters: StringValuesBuilder): Parameters = ParametersBuilder() - .apply { appendAllDecoded(parameters) } - .build() - -internal fun encodeParameters(parameters: StringValues): ParametersBuilder = ParametersBuilder() - .apply { appendAllEncoded(parameters) } - -private fun StringValuesBuilder.appendAllDecoded(parameters: StringValuesBuilder) { - parameters.names() - .forEach { key -> - val values = parameters.getAll(key) ?: emptyList() - appendAll( - key.decodeURLQueryComponent(), - values.map { it.decodeURLQueryComponent(plusIsSpace = true) } - ) - } -} - -private fun StringValuesBuilder.appendAllEncoded(parameters: StringValues) { - parameters.names() - .forEach { key -> - val values = parameters.getAll(key) ?: emptyList() - appendAll(key.encodeURLParameter(), values.map { it.encodeURLParameterValue() }) - } -} diff --git a/ktor-http/common/src/io/ktor/http/UrlEncodedParametersBuilder.kt b/ktor-http/common/src/io/ktor/http/UrlEncodedParametersBuilder.kt new file mode 100644 index 00000000000..2d60fbe8aec --- /dev/null +++ b/ktor-http/common/src/io/ktor/http/UrlEncodedParametersBuilder.kt @@ -0,0 +1,207 @@ +/* + * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.http + +import io.ktor.util.* + +internal class UrlEncodedParametersBuilder( + private val rawEncodedParametersBuilder: ParametersBuilder, + private val decodedParametersBuilder: ParametersBuilder +) : ParametersBuilder { + + override fun build(): Parameters { + return ParametersBuilder() + .apply { + appendAllEncoded(decodedParametersBuilder.build()) + appendAll(rawEncodedParametersBuilder) + }.build() + } + + override val caseInsensitiveName: Boolean = decodedParametersBuilder.caseInsensitiveName + + override fun getAll(name: String): List? { + val result = mutableListOf() + if (name.isDecodableToUTF8String()) { + decodedParametersBuilder.getAll(name.decodeURLParameter())?.let { values -> + result.addAll(values.map { value -> value.encodeURLParameterValue() }) + } + } + rawEncodedParametersBuilder.getAll(name)?.let { + values -> result.addAll(values) + } + + return if (result.isEmpty()) null else result + } + + override fun contains(name: String): Boolean { + val result = if (name.isDecodableToUTF8String()) { + decodedParametersBuilder.contains(name.decodeURLParameter()) + } else false + return result || rawEncodedParametersBuilder.contains(name) + } + + override fun contains(name: String, value: String): Boolean { + val result = if (name.isDecodableToUTF8String() && value.isDecodableToUTF8String(plusIsSpace = true)) { + decodedParametersBuilder.contains(name.decodeURLParameter(), value.decodeURLParameterValue()) + } else false + return result || rawEncodedParametersBuilder.contains(name, value) + } + + override fun names(): Set = mutableSetOf().apply{ + addAll(decodedParametersBuilder.names().encode()) + addAll(rawEncodedParametersBuilder.names()) + } + + override fun isEmpty(): Boolean = rawEncodedParametersBuilder.isEmpty() && decodedParametersBuilder.isEmpty() + + override fun entries(): Set>> { + val entryTable = hashMapOf>() + rawEncodedParametersBuilder.entries().forEach { + entryTable[it.key] = it.value + } + decodedParametersBuilder.entries().forEach { + val key = it.key.encodeURLParameter() + val values = it.value.map { v -> v.encodeURLParameterValue() } + + entryTable[key]?.let { v -> + val newValues = mutableListOf() + newValues.addAll(v) + newValues.addAll(values) + entryTable[key] = newValues + } ?: let { entryTable[key] = values } + } + return entryTable.entries + } + + override fun set(name: String, value: String) { + val nameIsDecodable = name.checkDecodableToUTF8String() + val valueIsDecodable = value.checkDecodableToUTF8String(plusIsSpace = true) + + if (nameIsDecodable && valueIsDecodable) { + decodedParametersBuilder[name.decodeURLParameter()] = value.decodeURLParameterValue() + rawEncodedParametersBuilder.remove(name) + } else { + rawEncodedParametersBuilder[name] = value + if (nameIsDecodable) { + decodedParametersBuilder.remove(name.decodeURLParameter()) + } + } + } + + override fun get(name: String): String? { + val result = if (name.isDecodableToUTF8String()) { + decodedParametersBuilder[name.decodeURLParameter()]?.encodeURLParameterValue() + } else null + return result ?: rawEncodedParametersBuilder[name] + } + + override fun append(name: String, value: String) { + if (name.checkDecodableToUTF8String() && value.checkDecodableToUTF8String(plusIsSpace = true)) { + decodedParametersBuilder.append(name.decodeURLParameter(), value.decodeURLParameterValue()) + } else { + rawEncodedParametersBuilder.append(name, value) + } + } + + override fun appendAll(stringValues: StringValues) { + stringValues.forEach { name, values -> appendAll(name, values) } + } + + override fun appendAll(name: String, values: Iterable) { + if (!name.checkDecodableToUTF8String()) { + rawEncodedParametersBuilder.appendAll(name, values) + } else { + val (decodedValues, encodedValues) = values.divide { it.checkDecodableToUTF8String(plusIsSpace = true) } + if (decodedValues.isNotEmpty() || encodedValues.isEmpty()) { + decodedParametersBuilder.appendAll( + name.decodeURLParameter(), + decodedValues.map { it.decodeURLParameterValue() }) + } + if (encodedValues.isNotEmpty()) { + rawEncodedParametersBuilder.appendAll(name, encodedValues) + } + } + } + + override fun appendMissing(stringValues: StringValues) { + stringValues.forEach { name, values -> appendMissing(name, values) } + } + + override fun appendMissing(name: String, values: Iterable) { + if (!name.checkDecodableToUTF8String()) { + rawEncodedParametersBuilder.appendMissing(name, values) + } else { + val (decodedValues, encodedValues) = values.divide { it.checkDecodableToUTF8String(plusIsSpace = true) } + decodedParametersBuilder.appendMissing( + name.decodeURLParameter(), + decodedValues.map { it.decodeURLParameterValue() }) + rawEncodedParametersBuilder.appendMissing(name, encodedValues) + } + } + + override fun remove(name: String) { + if (name.isDecodableToUTF8String()) { + decodedParametersBuilder.remove(name.decodeURLParameter()) + } + rawEncodedParametersBuilder.remove(name) + } + + override fun remove(name: String, value: String): Boolean { + return if (name.isDecodableToUTF8String() && value.isDecodableToUTF8String(plusIsSpace = true)) { + decodedParametersBuilder.remove(name.decodeURLParameter(), value.decodeURLParameterValue()) + } else { + rawEncodedParametersBuilder.remove(name, value) + } + } + + override fun removeKeysWithNoEntries() { + rawEncodedParametersBuilder.removeKeysWithNoEntries() + decodedParametersBuilder.removeKeysWithNoEntries() + } + + override fun clear() { + rawEncodedParametersBuilder.clear() + decodedParametersBuilder.clear() + } +} + +internal fun encodeParameters(parameters: StringValues): ParametersBuilder = ParametersBuilder() + .apply { appendAllEncoded(parameters) } + +private fun StringValuesBuilder.appendAllEncoded(parameters: StringValues) { + parameters.names() + .forEach { key -> + val values = parameters.getAll(key) ?: emptyList() + appendAll(key.encodeURLParameter(), values.map { it.encodeURLParameterValue() }) + } +} + +@Suppress("UNCHECKED_CAST") +private fun > T.encode(plusToSpace: Boolean = false): T { + val collection = when (this) { + is List<*> -> mutableListOf() + is Set<*> -> mutableSetOf() + else -> throw RuntimeException("Unsupported collection type ${this::class.qualifiedName}") + } + for (value in this) { + collection.add(value.encodeURLParameter(plusToSpace)) + } + return collection as T +} + +private fun Iterable.divide(predicate: (String) -> Boolean): Pair, List> { + val positive = mutableListOf() + val negative = mutableListOf() + + forEach { + if (predicate(it)) { + positive.add(it) + } else { + negative.add(it) + } + } + + return Pair(positive, negative) +} diff --git a/ktor-http/common/test/io/ktor/tests/http/CodecTest.kt b/ktor-http/common/test/io/ktor/tests/http/CodecTest.kt index 669b9c49ad1..d9ca383f59d 100644 --- a/ktor-http/common/test/io/ktor/tests/http/CodecTest.kt +++ b/ktor-http/common/test/io/ktor/tests/http/CodecTest.kt @@ -152,6 +152,31 @@ class CodecTest { fun testEncodeURLPathSurrogateSymbol() { assertEquals("/path/%F0%9F%90%95", surrogateSymbolUrlPath.encodeURLPath()) } + + @Test + fun testIsDecodableToUTF8String() { + assertTrue("".isDecodableToUTF8String()) + assertTrue("Simple".isDecodableToUTF8String()) + assertTrue("%D0%92%D1%81%D0%B5%D0%BC_%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82".isDecodableToUTF8String()) + assertTrue("Gr%C3%BCezi_z%C3%A4m%C3%A4".isDecodableToUTF8String()) + + assertFalse("+".isDecodableToUTF8String()) + assertTrue("+".isDecodableToUTF8String(plusIsSpace = true)) + assertFalse("%XX".isDecodableToUTF8String()) + assertFalse("%D+a".isDecodableToUTF8String()) + assertFalse("%D+a".isDecodableToUTF8String(plusIsSpace = true)) + + assertFalse("%D0".isDecodableToUTF8String()) // incomplete utf-8 sequence + assertFalse("%D0%D0%92".isDecodableToUTF8String()) // incomplete utf-8 sequence + assertFalse("%D0a".isDecodableToUTF8String()) // incomplete utf-8 sequence + assertFalse("%D0%92%92".isDecodableToUTF8String()) // extra utf-8 trailing byte + assertFalse("%D0+%92".isDecodableToUTF8String()) // split utf-8 sequence + assertFalse("%D0+%92".isDecodableToUTF8String(plusIsSpace = true)) // split utf-8 sequence + assertFalse("%92".isDecodableToUTF8String()) // utf-8 trailing byte without a header + assertFalse("%ED%A0%80".isDecodableToUTF8String()) // forbidden (U+D800 - U+DFFF) + assertFalse("%F4%90%80%80".isDecodableToUTF8String()) // forbidden (U+110000 and above) + assertFalse("%C0%BF".isDecodableToUTF8String()) // overlong encoding for %3F + } private fun encodeAndDecodeTest(text: String) { val encode1 = text.encodeURLQueryComponent() diff --git a/ktor-http/common/test/io/ktor/tests/http/URLBuilderTest.kt b/ktor-http/common/test/io/ktor/tests/http/URLBuilderTest.kt index 13a1700f162..cb40c1b407c 100644 --- a/ktor-http/common/test/io/ktor/tests/http/URLBuilderTest.kt +++ b/ktor-http/common/test/io/ktor/tests/http/URLBuilderTest.kt @@ -335,6 +335,9 @@ internal class URLBuilderTest { urlBuilder.parameters.append("as%df", "as df") assertEquals("as+df", urlBuilder.encodedParameters["as%25df"]) + + urlBuilder.encodedParameters.append("as%D0df", "as%83df") + assertEquals("as%83df", urlBuilder.encodedParameters["as%D0df"]) } @Test diff --git a/ktor-http/common/test/io/ktor/tests/http/UrlDecodedParametersBuilderTest.kt b/ktor-http/common/test/io/ktor/tests/http/UrlDecodedParametersBuilderTest.kt deleted file mode 100644 index 0c383fdca62..00000000000 --- a/ktor-http/common/test/io/ktor/tests/http/UrlDecodedParametersBuilderTest.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. - */ - -package io.ktor.tests.http - -import io.ktor.http.* -import kotlin.test.* - -class UrlDecodedParametersBuilderTest { - - @Test - fun testMethodsDelegateToEncoded() { - val encoded = ParametersBuilder() - val decoded = UrlDecodedParametersBuilder(encoded) - - encoded.append("key", "value") - assertFalse(decoded.isEmpty()) - - encoded.clear() - assertTrue(decoded.isEmpty()) - } - - @Test - fun testAppendDecoded() { - val encoded = ParametersBuilder() - val decoded = UrlDecodedParametersBuilder(encoded) - - decoded.append("ke y", "valu e") - assertEquals("valu+e", encoded["ke%20y"]) - assertEquals("valu e", decoded["ke y"]) - } - - @Test - fun testAppendAllDecoded() { - val encoded = ParametersBuilder() - val decoded = UrlDecodedParametersBuilder(encoded) - - decoded.appendAll("ke y", listOf("valu e", "v%alue")) - assertEquals(listOf("valu+e", "v%25alue"), encoded.getAll("ke%20y")) - assertEquals(listOf("valu e", "v%alue"), decoded.getAll("ke y")) - } - - @Test - fun testContainsDecoded() { - val encoded = ParametersBuilder() - val decoded = UrlDecodedParametersBuilder(encoded) - - decoded.append("ke y", "valu e") - assertTrue(encoded.contains("ke%20y")) - assertTrue(decoded.contains("ke y")) - assertTrue(encoded.contains("ke%20y", "valu+e")) - assertTrue(decoded.contains("ke y", "valu e")) - assertFalse(encoded.contains("ke%20y", "valu+e1")) - assertFalse(decoded.contains("ke y", "valu e1")) - } - - @Test - fun testNamesDecoded() { - val encoded = ParametersBuilder() - val decoded = UrlDecodedParametersBuilder(encoded) - - decoded.append("ke y", "valu e") - decoded.append("ke y1", "valu e1") - assertEquals(setOf("ke%20y", "ke%20y1"), encoded.names()) - assertEquals(setOf("ke y", "ke y1"), decoded.names()) - } - - @Test - fun testRemoveDecoded() { - val encoded = ParametersBuilder() - val decoded = UrlDecodedParametersBuilder(encoded) - - decoded.append("ke y", "valu e") - decoded.append("ke y", "valu e1") - decoded.remove("ke y", "valu e1") - assertTrue(encoded.contains("ke%20y", "valu+e")) - assertFalse(encoded.contains("ke%20y", "valu+e1")) - decoded.remove("ke y") - assertFalse(encoded.contains("ke%20y")) - } - - @Test - fun testEntriesDecoded() { - val encoded = ParametersBuilder() - val decoded = UrlDecodedParametersBuilder(encoded) - - decoded.append("ke y", "valu e") - decoded.append("ke y", "valu e1") - decoded.append("ke y1", "valu e1") - val entriesEncoded = encoded.entries() - val entriesDecoded = decoded.entries() - - assertEquals(2, entriesEncoded.size) - assertEquals(2, entriesDecoded.size) - - assertEquals(listOf("valu+e", "valu+e1"), entriesEncoded.single { it.key == "ke%20y" }.value) - assertEquals(listOf("valu+e1"), entriesEncoded.single { it.key == "ke%20y1" }.value) - - assertEquals(listOf("valu e", "valu e1"), entriesDecoded.single { it.key == "ke y" }.value) - assertEquals(listOf("valu e1"), entriesDecoded.single { it.key == "ke y1" }.value) - } - - @Test - fun testAppendStringValueDecoded() { - val encoded = ParametersBuilder() - val decoded = UrlDecodedParametersBuilder(encoded) - - val values = parametersOf("ke y" to listOf("valu e", "valu e1"), "ke y1" to listOf("valu e1")) - decoded.appendAll(values) - - assertEquals(2, encoded.entries().size) - assertEquals(2, decoded.entries().size) - - assertEquals(listOf("valu+e", "valu+e1"), encoded.getAll("ke%20y")) - assertEquals(listOf("valu+e1"), encoded.getAll("ke%20y1")) - - assertEquals(listOf("valu e", "valu e1"), decoded.getAll("ke y")) - assertEquals(listOf("valu e1"), decoded.getAll("ke y1")) - } - - @Test - fun testBuildDecoded() { - val encoded = ParametersBuilder() - val decoded = UrlDecodedParametersBuilder(encoded) - - decoded.appendAll("ke y", listOf("valu e", "valu e1")) - decoded.appendAll("ke y1", listOf("valu e1")) - - val result = decoded.build() - - assertEquals(listOf("valu e", "valu e1"), result.getAll("ke y")) - assertEquals(listOf("valu e1"), result.getAll("ke y1")) - } -} diff --git a/ktor-http/common/test/io/ktor/tests/http/UrlEncodedParametersBuilderTest.kt b/ktor-http/common/test/io/ktor/tests/http/UrlEncodedParametersBuilderTest.kt new file mode 100644 index 00000000000..958a54c52aa --- /dev/null +++ b/ktor-http/common/test/io/ktor/tests/http/UrlEncodedParametersBuilderTest.kt @@ -0,0 +1,293 @@ +/* + * Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.tests.http + +import io.ktor.http.* +import kotlin.test.* + +class UrlEncodedParametersBuilderTest { + + @Test + fun testMethodsDelegateToDecoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.append("key", "value") + assertFalse(decoded.isEmpty()) + assertTrue(rawEncoded.isEmpty()) + + encoded.clear() + assertTrue(decoded.isEmpty()) + } + + @Test + fun testMethodsDelegateToRawEncoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.append("key%D0", "value%83") + assertFalse(rawEncoded.isEmpty()) + assertTrue(decoded.isEmpty()) + + encoded.clear() + assertTrue(rawEncoded.isEmpty()) + } + + @Test + fun testAppendDecoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + decoded.append("ke y", "valu e") + assertEquals("valu+e", encoded["ke%20y"]) + assertEquals("valu e", decoded["ke y"]) + } + + @Test + fun testAppendEncoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.append("key%D0", "valu%20e") + assertEquals("valu%20e", rawEncoded["key%D0"]) + assertEquals("valu%20e", encoded["key%D0"]) + + assertFails { + encoded.append("key", "invalid encoding %%") + } + } + + @Test + fun testAppendAllDecoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + decoded.appendAll("ke y", listOf("valu e", "v%alue")) + assertEquals(listOf("valu+e", "v%25alue"), encoded.getAll("ke%20y")) + assertEquals(listOf("valu e", "v%alue"), decoded.getAll("ke y")) + } + + @Test + fun testAppendAllEncoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.appendAll("ke%D0y", listOf("valu+e", "v%25alue")) + assertEquals(listOf("valu+e", "v%25alue"), rawEncoded.getAll("ke%D0y")) + assertEquals(listOf("valu+e", "v%25alue"), encoded.getAll("ke%D0y")) + + encoded.appendAll("ke%20y", listOf("valu%D0e", "value%83")) + assertEquals(listOf("valu%D0e", "value%83"), rawEncoded.getAll("ke%20y")) + assertEquals(listOf("valu%D0e", "value%83"), encoded.getAll("ke%20y")) + + assertFails { + encoded.appendAll("key", listOf("value", "invalid encoding %%")) + } + } + + @Test + fun testContainsDecoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + decoded.append("ke y", "valu e") + assertTrue(encoded.contains("ke%20y")) + assertTrue(decoded.contains("ke y")) + assertTrue(encoded.contains("ke%20y", "valu+e")) + assertTrue(decoded.contains("ke y", "valu e")) + assertFalse(encoded.contains("ke%20y", "valu+e1")) + assertFalse(decoded.contains("ke y", "valu e1")) + } + + @Test + fun testContainsEncoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.append("ke%D0y", "valu+e") + assertTrue(encoded.contains("ke%D0y")) + assertTrue(rawEncoded.contains("ke%D0y")) + assertTrue(encoded.contains("ke%D0y", "valu+e")) + assertTrue(rawEncoded.contains("ke%D0y", "valu+e")) + assertFalse(encoded.contains("ke%D0y", "valu+e1")) + assertFalse(rawEncoded.contains("ke%D0y", "valu+e1")) + } + + @Test + fun testNamesDecoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + decoded.append("ke y", "valu e") + decoded.append("ke y1", "valu e1") + assertEquals(setOf("ke%20y", "ke%20y1"), encoded.names()) + assertEquals(setOf("ke y", "ke y1"), decoded.names()) + } + + @Test + fun testNamesEncoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.append("ke%20y", "value%83") + encoded.append("ke%20y1", "value1%83") + encoded.append("ke%D0y2", "value+2") + encoded.append("ke%D0y3", "value3") + assertEquals(setOf("ke%20y", "ke%20y1", "ke%D0y2", "ke%D0y3"), encoded.names()) + assertEquals(setOf("ke%20y", "ke%20y1", "ke%D0y2", "ke%D0y3"), rawEncoded.names()) + } + + @Test + fun testRemoveDecoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + decoded.append("ke y", "valu e") + decoded.append("ke y", "valu e1") + decoded.remove("ke y", "valu e1") + assertTrue(encoded.contains("ke%20y", "valu+e")) + assertFalse(encoded.contains("ke%20y", "valu+e1")) + decoded.remove("ke y") + assertFalse(encoded.contains("ke%20y")) + } + + @Test + fun testRemoveEncoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.append("ke%20y", "valu+e%D0") + encoded.append("ke%20y", "valu+e1%83") + encoded.remove("ke%20y", "valu+e1%83") + assertTrue(encoded.contains("ke%20y", "valu+e%D0")) + assertFalse(encoded.contains("ke%20y", "valu+e1%83")) + encoded.remove("ke%20y") + assertFalse(encoded.contains("ke%20y")) + } + + @Test + fun testEntriesDecoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + decoded.append("ke y", "valu e") + decoded.append("ke y", "valu e1") + decoded.append("ke y1", "valu e1") + val entriesEncoded = encoded.entries() + val entriesDecoded = decoded.entries() + + assertEquals(2, entriesEncoded.size) + assertEquals(2, entriesDecoded.size) + + assertEquals(listOf("valu+e", "valu+e1"), entriesEncoded.single { it.key == "ke%20y" }.value) + assertEquals(listOf("valu+e1"), entriesEncoded.single { it.key == "ke%20y1" }.value) + + assertEquals(listOf("valu e", "valu e1"), entriesDecoded.single { it.key == "ke y" }.value) + assertEquals(listOf("valu e1"), entriesDecoded.single { it.key == "ke y1" }.value) + } + + @Test + fun testEntriesEncoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.append("ke%20y%D0", "valu+e") + encoded.append("ke%20y%D0", "valu+e1") + encoded.append("ke%20y1", "valu+e1%83") + val entriesEncoded = encoded.entries() + val entriesRawEncoded = rawEncoded.entries() + + assertEquals(2, entriesEncoded.size) + assertEquals(2, entriesRawEncoded.size) + + assertEquals(listOf("valu+e", "valu+e1"), entriesEncoded.single { it.key == "ke%20y%D0" }.value) + assertEquals(listOf("valu+e1%83"), entriesEncoded.single { it.key == "ke%20y1" }.value) + + assertEquals(listOf("valu+e", "valu+e1"), entriesRawEncoded.single { it.key == "ke%20y%D0" }.value) + assertEquals(listOf("valu+e1%83"), entriesRawEncoded.single { it.key == "ke%20y1" }.value) + } + + @Test + fun testAppendStringValueDecoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + val values = parametersOf("ke y" to listOf("valu e", "valu e1"), "ke y1" to listOf("valu e1")) + decoded.appendAll(values) + + assertEquals(2, encoded.entries().size) + assertEquals(2, decoded.entries().size) + + assertEquals(listOf("valu+e", "valu+e1"), encoded.getAll("ke%20y")) + assertEquals(listOf("valu+e1"), encoded.getAll("ke%20y1")) + + assertEquals(listOf("valu e", "valu e1"), decoded.getAll("ke y")) + assertEquals(listOf("valu e1"), decoded.getAll("ke y1")) + } + + @Test + fun testAppendStringValueEncoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + val values = parametersOf("ke%20y" to listOf("valu+e%D0", "valu+e1%83"), "ke%20y1%D0" to listOf("valu+e1")) + encoded.appendAll(values) + + assertEquals(2, encoded.entries().size) + assertEquals(2, rawEncoded.entries().size) + + assertEquals(listOf("valu+e%D0", "valu+e1%83"), encoded.getAll("ke%20y")) + assertEquals(listOf("valu+e1"), encoded.getAll("ke%20y1%D0")) + + assertEquals(listOf("valu+e%D0", "valu+e1%83"), rawEncoded.getAll("ke%20y")) + assertEquals(listOf("valu+e1"), rawEncoded.getAll("ke%20y1%D0")) + } + + @Test + fun testBuildDecoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.appendAll("ke%20y", listOf("valu+e", "valu+e1")) + encoded.appendAll("ke%20y1", listOf("valu+e1")) + + val result = decoded.build() + + assertEquals(listOf("valu e", "valu e1"), result.getAll("ke y")) + assertEquals(listOf("valu e1"), result.getAll("ke y1")) + } + + @Test + fun testBuildEncoded() { + val rawEncoded = ParametersBuilder() + val decoded = ParametersBuilder() + val encoded = UrlEncodedParametersBuilder(rawEncoded, decoded) + + encoded.appendAll("ke%20y%83", listOf("valu+e", "valu+e1")) + encoded.appendAll("ke%20y1", listOf("valu+e1%D0")) + + val result = encoded.build() + + assertEquals(listOf("valu+e", "valu+e1"), result.getAll("ke%20y%83")) + assertEquals(listOf("valu+e1%D0"), result.getAll("ke%20y1")) + } +} From 83b40e89597f71389d6aed36a4685243980130fb Mon Sep 17 00:00:00 2001 From: Dmitry Vorobiev Date: Tue, 9 Jul 2024 10:21:00 +0300 Subject: [PATCH 2/2] KTOR-7190: Fix code formatting --- ktor-http/common/src/io/ktor/http/Codecs.kt | 30 +++++++++---------- .../ktor/http/UrlEncodedParametersBuilder.kt | 23 +++++++++----- .../test/io/ktor/tests/http/CodecTest.kt | 22 +++++++------- 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/ktor-http/common/src/io/ktor/http/Codecs.kt b/ktor-http/common/src/io/ktor/http/Codecs.kt index 7c3f9155056..04f850b4322 100644 --- a/ktor-http/common/src/io/ktor/http/Codecs.kt +++ b/ktor-http/common/src/io/ktor/http/Codecs.kt @@ -151,7 +151,7 @@ private fun String.isDecodable(plusIsSpace: Boolean = false): Decodability { for (char in this) { if (char == '%') { if (remainingPercentChars != 0) { - decodability = Decodability.UNDECODABLE // "%0%"-like pattern + decodability = Decodability.UNDECODABLE // "%0%"-like pattern break } remainingPercentChars = 2 @@ -182,7 +182,7 @@ private fun String.isDecodable(plusIsSpace: Boolean = false): Decodability { 2 -> percentByte and 0x0f 3 -> percentByte and 0x07 else -> { - decodability = Decodability.UNDECODABLE // unreachable + decodability = Decodability.UNDECODABLE // unreachable break } } @@ -193,8 +193,8 @@ private fun String.isDecodable(plusIsSpace: Boolean = false): Decodability { unicodeCharCode = unicodeCharCode or (percentByte and 0x3f) if (remainingUtf8Bytes == 0) { // Check forbidden code points - if (unicodeCharCode in 0xd800..0xdfff) return Decodability.BINARY // invalid UTF-8 sequence - if (unicodeCharCode >= 0x110000) return Decodability.BINARY // invalid UTF-8 sequence + if (unicodeCharCode in 0xd800..0xdfff) return Decodability.BINARY // invalid UTF-8 sequence + if (unicodeCharCode >= 0x110000) return Decodability.BINARY // invalid UTF-8 sequence // Check overlong encoding when { @@ -202,45 +202,45 @@ private fun String.isDecodable(plusIsSpace: Boolean = false): Decodability { expectedUtf8Bytes == 1 && unicodeCharCode in 0x80..0x07ff -> Unit expectedUtf8Bytes == 2 && unicodeCharCode in 0x0800..0xffff -> Unit expectedUtf8Bytes == 3 && unicodeCharCode in 0x010000..0x10ffff -> Unit - else -> decodability = Decodability.BINARY // invalid UTF-8 sequence + else -> decodability = Decodability.BINARY // invalid UTF-8 sequence } } } else { // Unexpected byte in the middle of UTF-8 sequence - decodability = Decodability.BINARY // invalid UTF-8 sequence + decodability = Decodability.BINARY // invalid UTF-8 sequence } } } else if (remainingUtf8Bytes != 0) { - decodability = Decodability.BINARY // invalid UTF-8 sequence + decodability = Decodability.BINARY // invalid UTF-8 sequence } else if (remainingPercentChars != 0) { - decodability = Decodability.UNDECODABLE // "%0x"-like pattern + decodability = Decodability.UNDECODABLE // "%0x"-like pattern break } } else if (char in SPECIAL_SYMBOLS_CHARS) { if (remainingUtf8Bytes != 0) { - decodability = Decodability.BINARY // invalid UTF-8 sequence + decodability = Decodability.BINARY // invalid UTF-8 sequence } else if (remainingPercentChars != 0) { - decodability = Decodability.UNDECODABLE // "%0~"-like pattern + decodability = Decodability.UNDECODABLE // "%0~"-like pattern break } } else if (char == '+') { if (!plusIsSpace || remainingPercentChars != 0) { - decodability = Decodability.UNDECODABLE // plus is threatened as invalid input symbol here or "%0+"-like pattern + decodability = Decodability.UNDECODABLE // plus is threatened as invalid input symbol here or "%0+"-like pattern break } else if (remainingUtf8Bytes != 0) { - decodability = Decodability.BINARY // invalid UTF-8 sequence + decodability = Decodability.BINARY // invalid UTF-8 sequence } } else { - decodability = Decodability.UNDECODABLE // invalid input symbol + decodability = Decodability.UNDECODABLE // invalid input symbol break } } if (decodability == Decodability.UTF8_STRING && remainingUtf8Bytes != 0) { - decodability = Decodability.BINARY // incomplete UTF-8 sequence + decodability = Decodability.BINARY // incomplete UTF-8 sequence } return if (remainingPercentChars != 0) { - Decodability.UNDECODABLE // "%0"-like pattern + Decodability.UNDECODABLE // "%0"-like pattern } else { decodability } diff --git a/ktor-http/common/src/io/ktor/http/UrlEncodedParametersBuilder.kt b/ktor-http/common/src/io/ktor/http/UrlEncodedParametersBuilder.kt index 2d60fbe8aec..059d14635ae 100644 --- a/ktor-http/common/src/io/ktor/http/UrlEncodedParametersBuilder.kt +++ b/ktor-http/common/src/io/ktor/http/UrlEncodedParametersBuilder.kt @@ -29,7 +29,8 @@ internal class UrlEncodedParametersBuilder( } } rawEncodedParametersBuilder.getAll(name)?.let { - values -> result.addAll(values) + values -> + result.addAll(values) } return if (result.isEmpty()) null else result @@ -38,18 +39,22 @@ internal class UrlEncodedParametersBuilder( override fun contains(name: String): Boolean { val result = if (name.isDecodableToUTF8String()) { decodedParametersBuilder.contains(name.decodeURLParameter()) - } else false + } else { + false + } return result || rawEncodedParametersBuilder.contains(name) } override fun contains(name: String, value: String): Boolean { val result = if (name.isDecodableToUTF8String() && value.isDecodableToUTF8String(plusIsSpace = true)) { decodedParametersBuilder.contains(name.decodeURLParameter(), value.decodeURLParameterValue()) - } else false + } else { + false + } return result || rawEncodedParametersBuilder.contains(name, value) } - override fun names(): Set = mutableSetOf().apply{ + override fun names(): Set = mutableSetOf().apply { addAll(decodedParametersBuilder.names().encode()) addAll(rawEncodedParametersBuilder.names()) } @@ -93,7 +98,9 @@ internal class UrlEncodedParametersBuilder( override fun get(name: String): String? { val result = if (name.isDecodableToUTF8String()) { decodedParametersBuilder[name.decodeURLParameter()]?.encodeURLParameterValue() - } else null + } else { + null + } return result ?: rawEncodedParametersBuilder[name] } @@ -117,7 +124,8 @@ internal class UrlEncodedParametersBuilder( if (decodedValues.isNotEmpty() || encodedValues.isEmpty()) { decodedParametersBuilder.appendAll( name.decodeURLParameter(), - decodedValues.map { it.decodeURLParameterValue() }) + decodedValues.map { it.decodeURLParameterValue() } + ) } if (encodedValues.isNotEmpty()) { rawEncodedParametersBuilder.appendAll(name, encodedValues) @@ -136,7 +144,8 @@ internal class UrlEncodedParametersBuilder( val (decodedValues, encodedValues) = values.divide { it.checkDecodableToUTF8String(plusIsSpace = true) } decodedParametersBuilder.appendMissing( name.decodeURLParameter(), - decodedValues.map { it.decodeURLParameterValue() }) + decodedValues.map { it.decodeURLParameterValue() } + ) rawEncodedParametersBuilder.appendMissing(name, encodedValues) } } diff --git a/ktor-http/common/test/io/ktor/tests/http/CodecTest.kt b/ktor-http/common/test/io/ktor/tests/http/CodecTest.kt index d9ca383f59d..e4303cd80de 100644 --- a/ktor-http/common/test/io/ktor/tests/http/CodecTest.kt +++ b/ktor-http/common/test/io/ktor/tests/http/CodecTest.kt @@ -152,7 +152,7 @@ class CodecTest { fun testEncodeURLPathSurrogateSymbol() { assertEquals("/path/%F0%9F%90%95", surrogateSymbolUrlPath.encodeURLPath()) } - + @Test fun testIsDecodableToUTF8String() { assertTrue("".isDecodableToUTF8String()) @@ -166,16 +166,16 @@ class CodecTest { assertFalse("%D+a".isDecodableToUTF8String()) assertFalse("%D+a".isDecodableToUTF8String(plusIsSpace = true)) - assertFalse("%D0".isDecodableToUTF8String()) // incomplete utf-8 sequence - assertFalse("%D0%D0%92".isDecodableToUTF8String()) // incomplete utf-8 sequence - assertFalse("%D0a".isDecodableToUTF8String()) // incomplete utf-8 sequence - assertFalse("%D0%92%92".isDecodableToUTF8String()) // extra utf-8 trailing byte - assertFalse("%D0+%92".isDecodableToUTF8String()) // split utf-8 sequence - assertFalse("%D0+%92".isDecodableToUTF8String(plusIsSpace = true)) // split utf-8 sequence - assertFalse("%92".isDecodableToUTF8String()) // utf-8 trailing byte without a header - assertFalse("%ED%A0%80".isDecodableToUTF8String()) // forbidden (U+D800 - U+DFFF) - assertFalse("%F4%90%80%80".isDecodableToUTF8String()) // forbidden (U+110000 and above) - assertFalse("%C0%BF".isDecodableToUTF8String()) // overlong encoding for %3F + assertFalse("%D0".isDecodableToUTF8String()) // incomplete utf-8 sequence + assertFalse("%D0%D0%92".isDecodableToUTF8String()) // incomplete utf-8 sequence + assertFalse("%D0a".isDecodableToUTF8String()) // incomplete utf-8 sequence + assertFalse("%D0%92%92".isDecodableToUTF8String()) // extra utf-8 trailing byte + assertFalse("%D0+%92".isDecodableToUTF8String()) // split utf-8 sequence + assertFalse("%D0+%92".isDecodableToUTF8String(plusIsSpace = true)) // split utf-8 sequence + assertFalse("%92".isDecodableToUTF8String()) // utf-8 trailing byte without a header + assertFalse("%ED%A0%80".isDecodableToUTF8String()) // forbidden (U+D800 - U+DFFF) + assertFalse("%F4%90%80%80".isDecodableToUTF8String()) // forbidden (U+110000 and above) + assertFalse("%C0%BF".isDecodableToUTF8String()) // overlong encoding for %3F } private fun encodeAndDecodeTest(text: String) {