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-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2024-2026 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
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.8/src/lib/fcitx-utils/stringutils.cpp#L362
// https://github.com/fcitx/fcitx5/blob/5.1.21/src/lib/fcitx-utils/stringutils.cpp#L100
// 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 {
val quoted = str.length >= 2 && str.first() == '"' && str.last() == '"'
val s = if (quoted) str.substring(1, str.length - 1) else str
if (s.isEmpty()) return s
var escape = false
return buildString {
s.forEach { c ->
when (escape) {
false -> {
if (c == '\\') {
escape = true
} else {
append(c)
}
if (str.length > 2 && str.startsWith('"') && str.endsWith('"')) {
val (consumed, result) = consumeMaybeEscapedValue(str)
return if (consumed.length == str.length) result else ""
}
return str
}
// https://en.cppreference.com/cpp/string/basic_string/find_first_of
private fun String.findFirstOf(str: String, pos: Int = 0): Int {
val set = str.toSet()
for (i in pos..<length) {
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 -> {
if (c == '\\') {
append('\\')
} else if (c == 'n') {
append('\n')
} else if (c == '"' && quoted) {
append('"')
} else {
throw IllegalStateException("Unexpected escape sequence '\\${c}' when unescaping string '${str}'.")
when (state) {
UnescapeState.NORMAL -> {
when (c) {
'\\' -> {
state = UnescapeState.ESCAPE
}
'"' -> {
end = i + 1
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 {
val needsQuote = str.lastIndexOfAny(QuotedChars) >= 0
val needsEscape = str.lastIndexOfAny(EscapeChars) >= 0
return buildString {
if (needsQuote) append('"')
if (needsEscape) append('"')
str.forEach { c ->
append(
when (c) {
'\\' -> "\\\\"
'\n' -> "\\n"
'"' -> "\\\""
else -> c
}
)
val escape = escapeMap[c]
if (escape != null) {
append('\\')
append(escape)
} else {
append(c)
}
}
if (needsQuote) append('"')
if (needsEscape) append('"')
}
}
}

View File

@ -1,6 +1,6 @@
/*
* 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
@ -11,13 +11,15 @@ import org.junit.Test
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(
"\"" to """"\""""",
"\"\"\n" to """"\"\"\n"""",
"abc" to """abc""",
"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