summaryrefslogtreecommitdiff
path: root/formats/json
diff options
context:
space:
mode:
authorRoberto Blázquez <elroby444@gmail.com>2023-05-17 12:12:36 +0200
committerGitHub <noreply@github.com>2023-05-17 12:12:36 +0200
commitf76d07314ead1db92f27c389494f537c12d3ea7b (patch)
tree72bd4f2b6beaeb9cc8e3c465cb14c2a17281924f /formats/json
parentf83385276ce63fd8f76ce333a6f43ec095e9a1da (diff)
downloadkotlinx.serialization-f76d07314ead1db92f27c389494f537c12d3ea7b.tar.gz
Add support to decode numeric literals containing an exponent (#2227)
Fixes #2078
Diffstat (limited to 'formats/json')
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt36
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/JsonElementSerializers.kt8
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/StringOps.kt2
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/lexer/AbstractJsonLexer.kt45
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