Skip to content

Commit

Permalink
Ensure that serialization exception is thrown from JSON parser on inv…
Browse files Browse the repository at this point in the history
…alid inputs

Fixes #704
  • Loading branch information
qwwdfsad committed Feb 26, 2020
1 parent 9ed6235 commit 1fecb99
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ import kotlin.jvm.*
* * [ignoreUnknownKeys] ignores encounters of unknown properties in the input JSON.
*
* * [isLenient] removes JSON specification restriction (RFC-4627) and makes parser
* more liberal to the malformed input. In lenient modes quoted string literals,
* more liberal to the malformed input. In lenient mode quoted boolean literals,
* and unquoted string literals are allowed.
*
* * [serializeSpecialFloatingPointValues] removes JSON specification restriction on
* special floating-point values such as `NaN` and `Infinity` and enables their
* serialization. When enabling it, please assure that the receiving party will be
* able to parse these special value.
* serialization. When enabling it, please ensure that the receiving party will be
* able to parse these special values.
*
* * [unquotedPrint] specifies whether keys and values should be quoted when building the
* JSON string. This option is intended to be used for debugging and pretty-printing,
Expand Down Expand Up @@ -114,6 +114,6 @@ public data class JsonConfiguration @UnstableDefault constructor(
"'ignoreUnknownKeys', 'isLenient' and 'serializeSpecialFloatingPointValues'")
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
@kotlin.internal.LowPriorityInOverloadResolution
fun JsonConfiguration(strictMode: Boolean = true, unquoted: Boolean = false) {
public fun JsonConfiguration(strictMode: Boolean = true, unquoted: Boolean = false) {
error("Should not be called")
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ public object JsonElementSerializer : KSerializer<JsonElement> {
}

override fun deserialize(decoder: Decoder): JsonElement {
verify(decoder)
val input = decoder as JsonInput
val input = decoder.asJsonInput()
return input.decodeJson()
}
}
Expand All @@ -66,11 +65,8 @@ public object JsonPrimitiveSerializer : KSerializer<JsonPrimitive> {
}

override fun deserialize(decoder: Decoder): JsonPrimitive {
verify(decoder)
val result = (decoder as JsonInput).decodeJson()
require(result is JsonPrimitive) {
"Unexpected JsonElement type: " + result::class
}
val result = decoder.asJsonInput().decodeJson()
if (result !is JsonPrimitive) throw JsonDecodingException(-1, "Unexpected JSON element, expected JsonPrimitive, had ${result::class}", result.toString())
return result
}
}
Expand Down Expand Up @@ -131,11 +127,8 @@ public object JsonLiteralSerializer : KSerializer<JsonLiteral> {
}

override fun deserialize(decoder: Decoder): JsonLiteral {
verify(decoder)
val result = (decoder as JsonInput).decodeJson()
require(result is JsonLiteral) {
"Unexpected JsonElement type: " + result::class
}
val result = decoder.asJsonInput().decodeJson()
if (result !is JsonLiteral) throw JsonDecodingException(-1, "Unexpected JSON element, expected JsonLiteral, had ${result::class}", result.toString())
return result
}
}
Expand Down Expand Up @@ -188,13 +181,21 @@ public object JsonArraySerializer : KSerializer<JsonArray> {
}

private fun verify(encoder: Encoder) {
require(encoder is JsonOutput) {
"Json element serializer can be used only by Json format, had $encoder"
}
encoder.asJsonOutput()
}

private fun verify(decoder: Decoder) {
require(decoder is JsonInput) {
"Json element serializer can be used only by Json format, had $decoder"
}
decoder.asJsonInput()
}

internal fun Decoder.asJsonInput(): JsonInput = this as? JsonInput
?: throw IllegalStateException(
"This serializer can be used only with Json format." +
"Expected Decoder to be JsonInput, got ${this::class}"
)

internal fun Encoder.asJsonOutput() = this as? JsonOutput
?: throw IllegalStateException(
"This serializer can be used only with Json format." +
"Expected Encoder to be JsonOutput, got ${this::class}"
)
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ package kotlinx.serialization.json
import kotlinx.serialization.*

/**
* Generic exception indicating a problem with JSON serialization.
* Generic exception indicating a problem with JSON serialization and deserialization.
*/
public open class JsonException(message: String) : SerializationException(message)

Expand Down Expand Up @@ -39,7 +39,9 @@ internal fun InvalidFloatingPoint(value: Number, key: String, type: String, outp

internal fun UnknownKeyException(key: String, input: String) = JsonDecodingException(
-1,
"JSON encountered unknown key: '$key'. You can enable 'ignoreUnknownKeys' property to ignore unknown keys. JSON input: $input")
"JSON encountered unknown key: '$key'. You can enable 'JsonConfiguration.ignoreUnknownKeys' property to ignore unknown keys.\n" +
" JSON input: ${input.minify()}"
)

internal fun InvalidKeyKindException(keyDescriptor: SerialDescriptor) = JsonEncodingException(
"Value of type '${keyDescriptor.serialName}' can't be used in JSON as a key in the map. " +
Expand All @@ -48,7 +50,7 @@ internal fun InvalidKeyKindException(keyDescriptor: SerialDescriptor) = JsonEnco
)

private fun String.minify(offset: Int = -1): String {
if (offset < 200) return this
if (length < 200) return this
if (offset == -1) {
val start = this.length - 60
if (start <= 0) return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,3 @@ public interface JsonInput : Decoder, CompositeDecoder {
*/
public fun decodeJson(): JsonElement
}

internal fun Decoder.asJsonInput() = this as? JsonInput
?: throw SerializationException(
"This serializer can be used only with Json format." +
"Expected Decoder to be JsonInput, got ${this::class}"
)
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,3 @@ public interface JsonOutput: Encoder, CompositeEncoder {
*/
public fun encodeJson(element: JsonElement)
}

internal fun Encoder.asJsonOutput() = this as? JsonOutput
?: throw SerializationException(
"This serializer can be used only with Json format." +
"Expected Encoder to be JsonOutput, got ${this::class}"
)
Original file line number Diff line number Diff line change
Expand Up @@ -225,16 +225,14 @@ internal class JsonReader(private val source: String) {
length = 0 // in buffer
var currentPosition = startPosition + 1
var lastPosition = currentPosition
val length = source.length
while (source[currentPosition] != STRING) {
if (currentPosition >= length) fail("Unexpected EOF", currentPosition)
if (source[currentPosition] == STRING_ESC) {
appendRange(source, lastPosition, currentPosition)
val newPosition = appendEsc(source, currentPosition + 1)
currentPosition = newPosition
lastPosition = newPosition
} else {
currentPosition++
} else if (++currentPosition >= source.length) {
fail("EOF", currentPosition)
}
}
if (lastPosition == startPosition + 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ internal class StreamingJsonInput internal constructor(

if (!configuration.ignoreUnknownKeys) {
reader.fail(
"Encountered an unknown key '$key'. You can enable 'ignoreUnknownKeys' property" +
"Encountered an unknown key '$key'. You can enable 'JsonConfiguration.ignoreUnknownKeys' property" +
" to ignore unknown keys"
)
} else {
Expand Down Expand Up @@ -165,13 +165,14 @@ internal class StreamingJsonInput internal constructor(
* The rest of the primitives are allowed to be quoted and unqouted
* to simplify integrations with third-party API.
*/
override fun decodeByte(): Byte = reader.takeString().toByte()
override fun decodeShort(): Short = reader.takeString().toShort()
override fun decodeInt(): Int = reader.takeString().toInt()
override fun decodeLong(): Long = reader.takeString().toLong()
override fun decodeFloat(): Float = reader.takeString().toFloat()
override fun decodeDouble(): Double = reader.takeString().toDouble()
override fun decodeChar(): Char = reader.takeString().single()
override fun decodeByte(): Byte = reader.takeString().parse("byte") { toByte() }
override fun decodeShort(): Short = reader.takeString().parse("short") { toShort() }
override fun decodeInt(): Int = reader.takeString().parse("int") { toInt() }
override fun decodeLong(): Long = reader.takeString().parse("long") { toLong() }
override fun decodeFloat(): Float = reader.takeString().parse("float") { toFloat() }
override fun decodeDouble(): Double = reader.takeString().parse("double") { toDouble() }
override fun decodeChar(): Char = reader.takeString().parse("char") { single() }

override fun decodeString(): String {
return if (configuration.isLenient) {
reader.takeString()
Expand All @@ -180,6 +181,14 @@ internal class StreamingJsonInput internal constructor(
}
}

private inline fun <T> String.parse(type: String, block: String.() -> T): T {
try {
return block()
} catch (e: Throwable) {
reader.fail("Failed to parse '$type'")
}
}

override fun decodeEnum(enumDescriptor: SerialDescriptor): Int {
return enumDescriptor.getElementIndexOrThrow(decodeString())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,6 @@ private sealed class AbstractJsonTreeInput(

protected abstract fun currentElement(tag: String): JsonElement

override fun decodeTaggedChar(tag: String): Char {
val o = getValue(tag)
return if (o.content.length == 1) o.content[0] else throw SerializationException("$o can't be represented as Char")
}

override fun decodeTaggedEnum(tag: String, enumDescription: SerialDescriptor): Int =
enumDescription.getElementIndexOrThrow(getValue(tag).content)

Expand All @@ -100,12 +95,22 @@ private sealed class AbstractJsonTreeInput(
return value.boolean
}

override fun decodeTaggedByte(tag: String): Byte = getValue(tag).int.toByte()
override fun decodeTaggedShort(tag: String) = getValue(tag).int.toShort()
override fun decodeTaggedInt(tag: String) = getValue(tag).int
override fun decodeTaggedLong(tag: String) = getValue(tag).long
override fun decodeTaggedFloat(tag: String) = getValue(tag).float
override fun decodeTaggedDouble(tag: String) = getValue(tag).double
override fun decodeTaggedByte(tag: String) = getValue(tag).primitive("byte") { int.toByte() }
override fun decodeTaggedShort(tag: String) = getValue(tag).primitive("short") { int.toShort() }
override fun decodeTaggedInt(tag: String) = getValue(tag).primitive("int") { int }
override fun decodeTaggedLong(tag: String) = getValue(tag).primitive("long") { long }
override fun decodeTaggedFloat(tag: String) = getValue(tag).primitive("float") { float }
override fun decodeTaggedDouble(tag: String) = getValue(tag).primitive("double") { double }
override fun decodeTaggedChar(tag: String): Char = getValue(tag).primitive("char") { content.single() }

private inline fun <T: Any> JsonPrimitive.primitive(primitive: String, block: JsonPrimitive.() -> T): T {
try {
return block()
} catch (e: Throwable) {
throw JsonDecodingException(-1, "Failed to parse '$primitive'", currentObject().toString())
}
}

override fun decodeTaggedString(tag: String): String {
val value = getValue(tag)
if (!json.configuration.isLenient) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2017-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.json

import kotlinx.serialization.*
import kotlin.test.*

class JsonParserFailureModesTest : JsonTestBase() {

@Serializable
data class Holder(
val id: Long
)

@Test
fun testFailureModes() = parametrizedTest {
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id": "}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id": ""}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id":a}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id":2.0}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id2":2}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id"}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"id}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"i}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{"}""", it) }
assertFailsWith<MissingFieldException> { Json.plain.parse(Holder.serializer(), """{}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """}""", it) }
assertFailsWith<JsonDecodingException> { Json.plain.parse(Holder.serializer(), """{""", it) }
}
}

0 comments on commit 1fecb99

Please sign in to comment.