Port new implementation of fcitx::stringutils::{,un}escapeForValue()

ffad9391ad
This commit is contained in:
Rocka 2026-07-26 16:55:30 +08:00
parent 10f62c6936
commit b1a030012d
No known key found for this signature in database
GPG Key ID: 28031158FFDD6853
2 changed files with 116 additions and 42 deletions

View File

@ -1,64 +1,136 @@
/* /*
* SPDX-License-Identifier: LGPL-2.1-or-later * SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors * SPDX-FileCopyrightText: Copyright 2024-2026 Fcitx5 for Android Contributors
*/ */
package org.fcitx.fcitx5.android.core package org.fcitx.fcitx5.android.core
object FcitxUtils { object FcitxUtils {
// https://github.com/fcitx/fcitx5/blob/5.1.8/src/lib/fcitx-utils/stringutils.cpp#L323 // https://github.com/fcitx/fcitx5/blob/5.1.21/src/lib/fcitx-utils/stringutils.cpp#L100
// https://github.com/fcitx/fcitx5/blob/5.1.8/src/lib/fcitx-utils/stringutils.cpp#L362 // https://en.cppreference.com/cpp/language/escape
private val unescapeMap = mapOf(
'\\' to '\\',
'"' to '"',
'n' to '\n',
'f' to '\u000c',
'r' to '\r',
't' to '\t',
'v' to '\u000b',
)
// https://github.com/fcitx/fcitx5/blob/5.1.21/src/lib/fcitx-utils/stringutils.cpp#L390
fun unescapeForValue(str: String): String { fun unescapeForValue(str: String): String {
val quoted = str.length >= 2 && str.first() == '"' && str.last() == '"' if (str.length > 2 && str.startsWith('"') && str.endsWith('"')) {
val s = if (quoted) str.substring(1, str.length - 1) else str val (consumed, result) = consumeMaybeEscapedValue(str)
if (s.isEmpty()) return s return if (consumed.length == str.length) result else ""
var escape = false }
return buildString { return str
s.forEach { c -> }
when (escape) {
false -> { // https://en.cppreference.com/cpp/string/basic_string/find_first_of
if (c == '\\') { private fun String.findFirstOf(str: String, pos: Int = 0): Int {
escape = true val set = str.toSet()
} else { for (i in pos..<length) {
append(c) if (set.contains(get(i))) {
} return i
}
}
return -1
}
// https://en.cppreference.com/cpp/string/basic_string/find_first_not_of
private fun String.findFirstNotOf(str: String, pos: Int = 0): Int {
val set = str.toSet()
for (i in pos..<length) {
if (!set.contains(get(i))) {
return i
}
}
return -1
}
enum class UnescapeState { NORMAL, ESCAPE }
// https://github.com/fcitx/fcitx5/blob/5.1.21/src/lib/fcitx-utils/stringutils.cpp#L435
private fun consumeMaybeEscapedValue(str: String, skip: String = ""): Pair<String, String> {
var input = str
val start = input.findFirstNotOf(skip)
if (start < 0) {
return "" to ""
}
input = input.substring(start)
val maybeQuoted = input.startsWith('"')
if (maybeQuoted) {
var end = 0
val result = buildString {
var state = UnescapeState.NORMAL
input.forEachIndexed { i, c ->
// skip first "
if (i == 0) {
return@forEachIndexed
} }
true -> { when (state) {
if (c == '\\') { UnescapeState.NORMAL -> {
append('\\') when (c) {
} else if (c == 'n') { '\\' -> {
append('\n') state = UnescapeState.ESCAPE
} else if (c == '"' && quoted) { }
append('"') '"' -> {
} else { end = i + 1
throw IllegalStateException("Unexpected escape sequence '\\${c}' when unescaping string '${str}'.") return@buildString
}
else -> {
append(c)
}
}
}
UnescapeState.ESCAPE -> {
// treat invalid escape sequence as normal character
append(unescapeMap[c] ?: c)
state = UnescapeState.NORMAL
} }
escape = false
} }
} }
} }
if (end > 0) {
val consumed = input.substring(0, end)
return consumed to result
}
} }
val end = input.findFirstOf(skip, 1)
val consumed = if (end < 0) input else input.substring(0, end)
return consumed to consumed
} }
private val QuotedChars = charArrayOf(' ', '"', '\t', '\r', '\u000b', '\u000c') // https://github.com/fcitx/fcitx5/blob/5.1.21/src/lib/fcitx-utils/stringutils.cpp#L86
private val escapeMap = mapOf(
'\\' to '\\',
'"' to '"',
'\n' to 'n',
'\u000c' to 'f',
'\r' to 'r',
'\t' to 't',
'\u000b' to 'v',
)
// https://github.com/fcitx/fcitx5/blob/5.1.8/src/lib/fcitx-utils/stringutils.cpp#L380 // "\f\r\t\v \"\\\n"
private val EscapeChars = charArrayOf('\u000c', '\r', '\t', '\u000b', ' ', '"', '\\', '\n')
// https://github.com/fcitx/fcitx5/blob/5.1.21/src/lib/fcitx-utils/stringutils.cpp#L404
fun escapeForValue(str: String): String { fun escapeForValue(str: String): String {
val needsQuote = str.lastIndexOfAny(QuotedChars) >= 0 val needsEscape = str.lastIndexOfAny(EscapeChars) >= 0
return buildString { return buildString {
if (needsQuote) append('"') if (needsEscape) append('"')
str.forEach { c -> str.forEach { c ->
append( val escape = escapeMap[c]
when (c) { if (escape != null) {
'\\' -> "\\\\" append('\\')
'\n' -> "\\n" append(escape)
'"' -> "\\\"" } else {
else -> c append(c)
} }
)
} }
if (needsQuote) append('"') if (needsEscape) append('"')
} }
} }
} }

View File

@ -1,6 +1,6 @@
/* /*
* SPDX-License-Identifier: LGPL-2.1-or-later * SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors * SPDX-FileCopyrightText: Copyright 2024-2026 Fcitx5 for Android Contributors
*/ */
package org.fcitx.fcitx5.android package org.fcitx.fcitx5.android
@ -11,13 +11,15 @@ import org.junit.Test
class StringEscapeTest { class StringEscapeTest {
// https://github.com/fcitx/fcitx5/blob/5.1.8/test/teststringutils.cpp#L118 // https://github.com/fcitx/fcitx5/blob/5.1.21/test/teststringutils.cpp#L142
private val data = listOf( private val data = listOf(
"\"" to """"\""""", "\"" to """"\""""",
"\"\"\n" to """"\"\"\n"""", "\"\"\n" to """"\"\"\n"""",
"abc" to """abc""", "abc" to """abc""",
"ab\"c" to """"ab\"c"""", "ab\"c" to """"ab\"c"""",
"a c" to """"a c"""" "a c" to """"a c"""",
"" to """"工 """",
"\r\u000b\u000c\n\t\\\" " to """"\r\v\f\n\t\\\" """",
) )
@Test @Test