Skip to content

Commit

Permalink
Add support to decode numeric literals containing an exponent (Kotlin…
Browse files Browse the repository at this point in the history
  • Loading branch information
xBaank authored and JesusMcCloud committed Jul 5, 2023
1 parent de4198b commit 124aa5d
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package kotlinx.serialization.json

import kotlinx.serialization.Serializable
import kotlinx.serialization.test.*
import kotlin.test.Test
import kotlin.test.assertEquals

class JsonExponentTest : JsonTestBase() {
@Serializable
data class SomeData(val count: Long)
@Serializable
data class SomeDataDouble(val count: Double)

@Test
fun testExponentDecodingPositive() = parametrizedTest {
val decoded = Json.decodeFromString<SomeData>("""{ "count": 23e11 }""", it)
assertEquals(2300000000000, decoded.count)
}

@Test
fun testExponentDecodingNegative() = parametrizedTest {
val decoded = Json.decodeFromString<SomeData>("""{ "count": -10E1 }""", it)
assertEquals(-100, decoded.count)
}

@Test
fun testExponentDecodingPositiveDouble() = parametrizedTest {
val decoded = Json.decodeFromString<SomeDataDouble>("""{ "count": 1.5E1 }""", it)
assertEquals(15.0, decoded.count)
}

@Test
fun testExponentDecodingNegativeDouble() = parametrizedTest {
val decoded = Json.decodeFromString<SomeDataDouble>("""{ "count": -1e-1 }""", it)
assertEquals(-0.1, decoded.count)
}

@Test
fun testExponentDecodingErrorTruncatedDecimal() = parametrizedTest {
assertFailsWithSerial("JsonDecodingException")
{ Json.decodeFromString<SomeData>("""{ "count": -1E-1 }""", it) }
}

@Test
fun testExponentDecodingErrorExponent() = parametrizedTest {
assertFailsWithSerial("JsonDecodingException")
{ Json.decodeFromString<SomeData>("""{ "count": 1e-1e-1 }""", it) }
}

@Test
fun testExponentDecodingErrorExponentDouble() = parametrizedTest {
assertFailsWithSerial("JsonDecodingException")
{ Json.decodeFromString<SomeDataDouble>("""{ "count": 1e-1e-1 }""", it) }
}

@Test
fun testExponentOverflowDouble() = parametrizedTest {
assertFailsWithSerial("JsonDecodingException")
{ Json.decodeFromString<SomeDataDouble>("""{ "count": 10000e10000 }""", it) }
}

@Test
fun testExponentUnderflowDouble() = parametrizedTest {
assertFailsWithSerial("JsonDecodingException")
{ Json.decodeFromString<SomeDataDouble>("""{ "count": -100e2222 }""", it) }
}

@Test
fun testExponentOverflow() = parametrizedTest {
assertFailsWithSerial("JsonDecodingException")
{ Json.decodeFromString<SomeData>("""{ "count": 10000e10000 }""", it) }
}

@Test
fun testExponentUnderflow() = parametrizedTest {
assertFailsWithSerial("JsonDecodingException")
{ Json.decodeFromString<SomeData>("""{ "count": -10000e10000 }""", it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,23 +255,35 @@ public val JsonElement.jsonNull: JsonNull
* Returns content of the current element as int
* @throws NumberFormatException if current element is not a valid representation of number
*/
public val JsonPrimitive.int: Int get() = content.toInt()
public val JsonPrimitive.int: Int
get() {
val result = mapExceptions { StringJsonLexer(content).consumeNumericLiteral() }
if (result !in Int.MIN_VALUE..Int.MAX_VALUE) throw NumberFormatException("$content is not an Int")
return result.toInt()
}

/**
* Returns content of the current element as int or `null` if current element is not a valid representation of number
*/
public val JsonPrimitive.intOrNull: Int? get() = content.toIntOrNull()
public val JsonPrimitive.intOrNull: Int?
get() {
val result = mapExceptionsToNull { StringJsonLexer(content).consumeNumericLiteral() } ?: return null
if (result !in Int.MIN_VALUE..Int.MAX_VALUE) return null
return result.toInt()
}

/**
* Returns content of current element as long
* @throws NumberFormatException if current element is not a valid representation of number
*/
public val JsonPrimitive.long: Long get() = content.toLong()
public val JsonPrimitive.long: Long get() = mapExceptions { StringJsonLexer(content).consumeNumericLiteral() }

/**
* Returns content of current element as long or `null` if current element is not a valid representation of number
*/
public val JsonPrimitive.longOrNull: Long? get() = content.toLongOrNull()
public val JsonPrimitive.longOrNull: Long?
get() =
mapExceptionsToNull { StringJsonLexer(content).consumeNumericLiteral() }

/**
* Returns content of current element as double
Expand Down Expand Up @@ -315,6 +327,22 @@ public val JsonPrimitive.contentOrNull: String? get() = if (this is JsonNull) nu
private fun JsonElement.error(element: String): Nothing =
throw IllegalArgumentException("Element ${this::class} is not a $element")

private inline fun <T> mapExceptionsToNull(f: () -> T): T? {
return try {
f()
} catch (e: JsonDecodingException) {
null
}
}

private inline fun <T> mapExceptions(f: () -> T): T {
return try {
f()
} catch (e: JsonDecodingException) {
throw NumberFormatException(e.message)
}
}

@PublishedApi
internal fun unexpectedJson(key: String, expected: String): Nothing =
throw IllegalArgumentException("Element $key is not a $expected")
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,18 @@ private object JsonLiteralSerializer : KSerializer<JsonLiteral> {
return encoder.encodeInline(value.coerceToInlineType).encodeString(value.content)
}

value.longOrNull?.let { return encoder.encodeLong(it) }
// use .content instead of .longOrNull as latter can process exponential notation,
// and it should be delegated to double when encoding.
value.content.toLongOrNull()?.let { return encoder.encodeLong(it) }

// most unsigned values fit to .longOrNull, but not ULong
value.content.toULongOrNull()?.let {
encoder.encodeInline(ULong.serializer().descriptor).encodeLong(it.toLong())
return
}

value.doubleOrNull?.let { return encoder.encodeDouble(it) }
value.booleanOrNull?.let { return encoder.encodeBoolean(it) }
value.content.toDoubleOrNull()?.let { return encoder.encodeDouble(it) }
value.content.toBooleanStrictOrNull()?.let { return encoder.encodeBoolean(it) }

encoder.encodeString(value.content)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ internal fun String.toBooleanStrictOrNull(): Boolean? = when {
this.equals("true", ignoreCase = true) -> true
this.equals("false", ignoreCase = true) -> false
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.serialization.json.internal.CharMappings.CHAR_TO_TOKEN
import kotlinx.serialization.json.internal.CharMappings.ESCAPE_2_CHAR
import kotlin.js.*
import kotlin.jvm.*
import kotlin.math.*

internal const val lenientHint = "Use 'isLenient = true' in 'Json {}` builder to accept non-compliant JSON."
internal const val coerceInputValuesHint = "Use 'coerceInputValues = true' in 'Json {}` builder to coerce nulls to default values."
Expand Down Expand Up @@ -601,11 +602,32 @@ internal abstract class AbstractJsonLexer {
false
}
var accumulator = 0L
var exponentAccumulator = 0L
var isNegative = false
var isExponentPositive = false
var hasExponent = false
val start = current
var hasChars = true
while (hasChars) {
while (current != source.length) {
val ch: Char = source[current]
if ((ch == 'e' || ch == 'E') && !hasExponent) {
if (current == start) fail("Unexpected symbol $ch in numeric literal")
isExponentPositive = true
hasExponent = true
++current
continue
}
if (ch == '-' && hasExponent) {
if (current == start) fail("Unexpected symbol '-' in numeric literal")
isExponentPositive = false
++current
continue
}
if (ch == '+' && hasExponent) {
if (current == start) fail("Unexpected symbol '+' in numeric literal")
isExponentPositive = true
++current
continue
}
if (ch == '-') {
if (current != start) fail("Unexpected symbol '-' in numeric literal")
isNegative = true
Expand All @@ -615,12 +637,16 @@ internal abstract class AbstractJsonLexer {
val token = charToTokenClass(ch)
if (token != TC_OTHER) break
++current
hasChars = current != source.length
val digit = ch - '0'
if (digit !in 0..9) fail("Unexpected symbol '$ch' in numeric literal")
if (hasExponent) {
exponentAccumulator = exponentAccumulator * 10 + digit
continue
}
accumulator = accumulator * 10 - digit
if (accumulator > 0) fail("Numeric value overflow")
}
val hasChars = current != start
if (start == current || (isNegative && start == current - 1)) {
fail("Expected numeric literal")
}
Expand All @@ -630,6 +656,19 @@ internal abstract class AbstractJsonLexer {
++current
}
currentPosition = current

fun calculateExponent(exponentAccumulator: Long, isExponentPositive: Boolean): Double = when (isExponentPositive) {
false -> 10.0.pow(-exponentAccumulator.toDouble())
true -> 10.0.pow(exponentAccumulator.toDouble())
}

if (hasExponent) {
val doubleAccumulator = accumulator.toDouble() * calculateExponent(exponentAccumulator, isExponentPositive)
if (doubleAccumulator > Long.MAX_VALUE || doubleAccumulator < Long.MIN_VALUE) fail("Numeric value overflow")
if (floor(doubleAccumulator) != doubleAccumulator) fail("Can't convert $doubleAccumulator to Long")
accumulator = doubleAccumulator.toLong()
}

return when {
isNegative -> accumulator
accumulator != Long.MIN_VALUE -> -accumulator
Expand Down

0 comments on commit 124aa5d

Please sign in to comment.