diff options
author | Roberto Blázquez <elroby444@gmail.com> | 2023-05-17 12:12:36 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-17 12:12:36 +0200 |
commit | f76d07314ead1db92f27c389494f537c12d3ea7b (patch) | |
tree | 72bd4f2b6beaeb9cc8e3c465cb14c2a17281924f /formats/json | |
parent | f83385276ce63fd8f76ce333a6f43ec095e9a1da (diff) | |
download | kotlinx.serialization-f76d07314ead1db92f27c389494f537c12d3ea7b.tar.gz |
Add support to decode numeric literals containing an exponent (#2227)
Fixes #2078
Diffstat (limited to 'formats/json')
4 files changed, 80 insertions, 11 deletions
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt index a325f76f..01359435 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt @@ -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 @@ -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") diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt index 83c23660..c95a7ba7 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt @@ -117,7 +117,9 @@ 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 { @@ -125,8 +127,8 @@ private object JsonLiteralSerializer : KSerializer<JsonLiteral> { 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) } diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StringOps.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StringOps.kt index 21bb8af9..f253d35a 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StringOps.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StringOps.kt @@ -70,4 +70,4 @@ internal fun String.toBooleanStrictOrNull(): Boolean? = when { this.equals("true", ignoreCase = true) -> true this.equals("false", ignoreCase = true) -> false else -> null -} +}
\ No newline at end of file diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt index 99afc4b3..58ed68d6 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt @@ -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." @@ -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 @@ -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") } @@ -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 |