Skip to content

Commit 287531a

Browse files
authored
fix(search): unpaired surrogate url encoded query (#407)
1 parent 4886188 commit 287531a

File tree

2 files changed

+53
-13
lines changed
  • client/src

2 files changed

+53
-13
lines changed

client/src/commonMain/kotlin/com/algolia/search/serialize/internal/Json.kt

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,11 @@ import com.algolia.search.model.multipleindex.IndexQuery
77
import com.algolia.search.model.multipleindex.MultipleQueriesStrategy
88
import com.algolia.search.model.search.Query
99
import com.algolia.search.model.settings.Settings
10-
import io.ktor.http.Parameters
11-
import io.ktor.http.formUrlEncode
12-
import io.ktor.util.InternalAPI
10+
import io.ktor.http.*
1311
import kotlinx.serialization.ExperimentalSerializationApi
1412
import kotlinx.serialization.encoding.Decoder
1513
import kotlinx.serialization.encoding.Encoder
16-
import kotlinx.serialization.json.Json
17-
import kotlinx.serialization.json.JsonArray
18-
import kotlinx.serialization.json.JsonDecoder
19-
import kotlinx.serialization.json.JsonElement
20-
import kotlinx.serialization.json.JsonEncoder
21-
import kotlinx.serialization.json.JsonObject
22-
import kotlinx.serialization.json.JsonPrimitive
23-
import kotlinx.serialization.json.jsonObject
14+
import kotlinx.serialization.json.*
2415

2516
internal val Json = Json {
2617
encodeDefaults = true
@@ -50,20 +41,36 @@ internal fun JsonObject.merge(jsonObject: JsonObject): JsonObject {
5041
}
5142
}
5243

53-
@OptIn(InternalAPI::class) // https://youtrack.jetbrains.com/issue/KT-48127
5444
internal fun JsonObject.urlEncode(): String? {
5545
return if (isNotEmpty()) {
5646
Parameters.build {
5747
entries.forEach { (key, element) ->
5848
when (element) {
59-
is JsonPrimitive -> append(key, element.content)
49+
is JsonPrimitive -> {
50+
append(key, sanitizeString(element.content))
51+
}
52+
6053
else -> append(key, Json.encodeToString(JsonElement.serializer(), element))
6154
}
6255
}
6356
}.formUrlEncode()
6457
} else null
6558
}
6659

60+
internal fun sanitizeString(input: String): String {
61+
return buildString {
62+
input.forEachIndexed { index, char ->
63+
val next = index + 1
64+
if (char.isHighSurrogate() && next < input.length && input[next].isLowSurrogate()) {
65+
append(char)
66+
append(input[next])
67+
} else if (!char.isSurrogate()) {
68+
append(char)
69+
}
70+
}
71+
}
72+
}
73+
6774
internal fun Query.toJsonNoDefaults(): JsonObject {
6875
return JsonNoDefaults.encodeToJsonElement(Query.serializer(), this).jsonObject
6976
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package serialize
2+
3+
import com.algolia.search.model.IndexName
4+
import com.algolia.search.model.internal.request.RequestMultipleQueries
5+
import com.algolia.search.model.multipleindex.IndexQuery
6+
import com.algolia.search.model.search.Query
7+
import com.algolia.search.serialize.internal.JsonNoDefaults
8+
import org.junit.Test
9+
import kotlin.test.assertEquals
10+
11+
class TestUnicode {
12+
13+
@Test
14+
fun pairedSurrogate() {
15+
val input = "Fox🦊"
16+
val query = RequestMultipleQueries(
17+
listOf(IndexQuery(IndexName("index_name"), Query(input)))
18+
)
19+
val encoded = JsonNoDefaults.encodeToString(RequestMultipleQueries.serializer(), query)
20+
assertEquals("""{"requests":[{"indexName":"index_name","params":"query=Fox%F0%9F%A6%8A"}]}""", encoded)
21+
}
22+
23+
@Test
24+
fun unpairedSurrogate() {
25+
val input = "Fox🦊"
26+
val trunked = input.substring(0, input.length - 1)
27+
val query = RequestMultipleQueries(
28+
listOf(IndexQuery(IndexName("index_name"), Query(trunked)))
29+
)
30+
val encoded = JsonNoDefaults.encodeToString(RequestMultipleQueries.serializer(), query)
31+
assertEquals("""{"requests":[{"indexName":"index_name","params":"query=Fox"}]}""", encoded)
32+
}
33+
}

0 commit comments

Comments
 (0)