Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,12 @@ class JsonParserTest : JsonTestBase() {
assertTrue { value.jsonPrimitive.isString }
assertEquals("null", obj["k"]!!.jsonPrimitive.content)
}

@Test
fun testUnicodeEscapeWithFollowingHex() {
// Test case for greedy parsing bug
val input = "\"\\u00f3a\""
val decoded = Json.decodeFromString<String>(input)
assertEquals("óa", decoded, "Should parse 'ó' then 'a', not try to consume 'a'")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,17 @@ internal abstract class AbstractJsonLexer {

protected abstract val source: CharSequence

// Lookup table for fast hex digit validation and conversion
companion object {
private val HEX_TABLE = IntArray(128) { -1 }

init {
for (i in '0'..'9') HEX_TABLE[i.code] = i - '0'
for (i in 'a'..'f') HEX_TABLE[i.code] = i - 'a' + 10
for (i in 'A'..'F') HEX_TABLE[i.code] = i - 'A' + 10
}
}

@JvmField
internal var currentPosition: Int = 0 // position in source

Expand Down Expand Up @@ -498,35 +509,40 @@ internal abstract class AbstractJsonLexer {
}

private fun appendHex(source: CharSequence, startPos: Int): Int {
// Ensure we have at least 4 characters for the unicode sequence
if (startPos + 4 >= source.length) {
currentPosition = startPos
ensureHaveChars()
if (currentPosition + 4 >= source.length)
fail("Unexpected EOF during unicode escape")
return appendHex(source, currentPosition)
}
escapedString.append(
((fromHexChar(source, startPos) shl 12) +
(fromHexChar(source, startPos + 1) shl 8) +
(fromHexChar(source, startPos + 2) shl 4) +
fromHexChar(source, startPos + 3)).toChar()
)

var value = 0
// Strict 4-iteration loop to prevent greedy parsing and comply with RFC 8259
for (i in 0..3) {
val char = source[startPos + i]
val code = char.code

// Fast O(1) lookup. Check range to avoid IndexOutOfBounds for non-ASCII chars
val digit = if (code < 128) HEX_TABLE[code] else -1

if (digit == -1) {
fail("Invalid Unicode escape sequence: expected hex digit, found '$char'")
}

// Accumulate result
value = (value shl 4) or digit
}

escapedString.append(value.toChar())
return startPos + 4
}

internal inline fun require(condition: Boolean, position: Int = currentPosition, message: () -> String) {
if (!condition) fail(message(), position)
}

private fun fromHexChar(source: CharSequence, currentPosition: Int): Int {
return when (val character = source[currentPosition]) {
in '0'..'9' -> character.code - '0'.code
in 'a'..'f' -> character.code - 'a'.code + 10
in 'A'..'F' -> character.code - 'A'.code + 10
else -> fail("Invalid toHexChar char '$character' in unicode escape")
}
}

fun skipElement(allowLenientStrings: Boolean) {
val tokenStack = mutableListOf<Byte>()
var lastToken = peekNextToken()
Expand Down Expand Up @@ -759,4 +775,4 @@ internal abstract class AbstractJsonLexer {
currentPosition = snapshot
}
}
}
}