Skip to content

Commit

Permalink
Fix parsing of responses with null result (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
keynmol authored Apr 11, 2024
1 parent 3bcbec6 commit 78ab76d
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 20 deletions.
11 changes: 8 additions & 3 deletions core/src/jsonrpclib/Codec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@ object Codec {
def decode[A](payload: Option[Payload])(implicit codec: Codec[A]): Either[ProtocolError, A] = codec.decode(payload)

implicit def fromJsonCodec[A](implicit jsonCodec: JsonValueCodec[A]): Codec[A] = new Codec[A] {
def encode(a: A): Payload = Payload(writeToArray(a))
def encode(a: A): Payload = {
Payload(writeToArray(a))
}

def decode(payload: Option[Payload]): Either[ProtocolError, A] = {
try {
payload match {
case Some(Payload(array)) => Right(readFromArray(array))
case None => Left(ProtocolError.ParseError("Expected to decode a payload"))
case Some(Payload.Data(payload)) => Right(readFromArray(payload))
case Some(Payload.NullPayload) => Right(readFromArray(nullArray))
case None => Left(ProtocolError.ParseError("Expected to decode a payload"))
}
} catch { case e: JsonReaderException => Left(ProtocolError.ParseError(e.getMessage())) }
}
}

private val nullArray = "null".getBytes()

}
39 changes: 29 additions & 10 deletions core/src/jsonrpclib/Payload.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,45 @@ import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec
import com.github.plokhotnyuk.jsoniter_scala.core.JsonWriter

import java.util.Base64
import jsonrpclib.Payload.Data
import jsonrpclib.Payload.NullPayload

final case class Payload(array: Array[Byte]) {
override def equals(other: Any) = other match {
case bytes: Payload => java.util.Arrays.equals(array, bytes.array)
case _ => false
sealed trait Payload extends Product with Serializable {
def stripNull: Option[Payload.Data] = this match {
case d @ Data(_) => Some(d)
case NullPayload => None
}

override lazy val hashCode: Int = java.util.Arrays.hashCode(array)

override def toString = Base64.getEncoder.encodeToString(array)
}

object Payload {
def apply(value: Array[Byte]) = {
if (value == null) NullPayload
else Data(value)
}
final case class Data(array: Array[Byte]) extends Payload {
override def equals(other: Any) = other match {
case bytes: Data => java.util.Arrays.equals(array, bytes.array)
case _ => false
}

override lazy val hashCode: Int = java.util.Arrays.hashCode(array)

override def toString = Base64.getEncoder.encodeToString(array)
}

case object NullPayload extends Payload

implicit val payloadJsonValueCodec: JsonValueCodec[Payload] = new JsonValueCodec[Payload] {
def decodeValue(in: JsonReader, default: Payload): Payload = {
Payload(in.readRawValAsBytes())
Data(in.readRawValAsBytes())
}

def encodeValue(bytes: Payload, out: JsonWriter): Unit =
out.writeRawVal(bytes.array)
bytes match {
case Data(array) => out.writeRawVal(array)
case NullPayload => out.writeNull()

}

def nullValue: Payload = null
}
Expand Down
10 changes: 6 additions & 4 deletions core/src/jsonrpclib/internals/RawMessage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package internals

import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
import com.github.plokhotnyuk.jsoniter_scala.macros.CodecMakerConfig

private[jsonrpclib] case class RawMessage(
jsonrpc: String,
method: Option[String] = None,
result: Option[Payload] = None,
result: Option[Option[Payload]] = None,
error: Option[ErrorPayload] = None,
params: Option[Payload] = None,
id: Option[CallId] = None
Expand All @@ -21,7 +22,7 @@ private[jsonrpclib] case class RawMessage(
case (Some(callId), None) =>
(error, result) match {
case (Some(error), _) => Right(OutputMessage.ErrorMessage(callId, error))
case (_, Some(data)) => Right(OutputMessage.ResponseMessage(callId, data))
case (_, Some(data)) => Right(OutputMessage.ResponseMessage(callId, data.getOrElse(Payload.NullPayload)))
case (None, None) =>
Left(
ProtocolError.InvalidRequest(
Expand All @@ -48,10 +49,11 @@ private[jsonrpclib] object RawMessage {
RawMessage(`2.0`, method = Some(method), params = params, id = Some(callId))
case OutputMessage.ErrorMessage(callId, errorPayload) =>
RawMessage(`2.0`, error = Some(errorPayload), id = Some(callId))
case OutputMessage.ResponseMessage(callId, data) => RawMessage(`2.0`, result = Some(data), id = Some(callId))
case OutputMessage.ResponseMessage(callId, data) =>
RawMessage(`2.0`, result = Some(data.stripNull), id = Some(callId))
}

implicit val rawMessageJsonValueCodecs: JsonValueCodec[RawMessage] =
JsonCodecMaker.make
JsonCodecMaker.make(CodecMakerConfig.withSkipNestedOptionValues(true))

}
30 changes: 30 additions & 0 deletions core/test/src-jvm-native/jsonrpclib/RawMessageSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package jsonrpclib

import munit._
import com.github.plokhotnyuk.jsoniter_scala.core._
import internals._
import jsonrpclib.CallId.NumberId
import jsonrpclib.OutputMessage.ResponseMessage

class RawMessageSpec() extends FunSuite {
test("json parsing with null result") {
// This is a perfectly valid response object, as result field has to be present,
// but can be null: https://www.jsonrpc.org/specification#response_object
val rawMessage = readFromString[RawMessage](""" {"jsonrpc":"2.0","result":null,"id":3} """.trim)
assertEquals(
rawMessage,
RawMessage(jsonrpc = "2.0", result = Some(None), id = Some(NumberId(3)))
)

assertEquals(rawMessage.toMessage, Right(ResponseMessage(NumberId(3), Payload.NullPayload)))

// This, on the other hand, is an invalid response message, as result field is missing
val invalidRawMessage = readFromString[RawMessage](""" {"jsonrpc":"2.0","id":3} """.trim)
assertEquals(
invalidRawMessage,
RawMessage(jsonrpc = "2.0", result = None, id = Some(NumberId(3)))
)

assert(invalidRawMessage.toMessage.isLeft, invalidRawMessage.toMessage)
}
}
12 changes: 9 additions & 3 deletions fs2/src/jsonrpclib/fs2/lsp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import jsonrpclib.Message
import jsonrpclib.ProtocolError
import jsonrpclib.Payload.Data
import jsonrpclib.Payload.NullPayload

object lsp {

Expand Down Expand Up @@ -42,11 +44,15 @@ object lsp {
}

private def writeChunk(payload: Payload): Chunk[Byte] = {
val size = payload.array.size
val header = s"Content-Length: ${size}" + "\r\n" * 2
Chunk.array(header.getBytes()) ++ Chunk.array(payload.array)
val bytes = payload match {
case Data(array) => array
case NullPayload => nullArray
}
val header = s"Content-Length: ${bytes.size}" + "\r\n" * 2
Chunk.array(header.getBytes()) ++ Chunk.array(bytes)
}

private val nullArray = "null".getBytes()
private val returnByte = '\r'.toByte
private val newlineByte = '\n'.toByte

Expand Down

0 comments on commit 78ab76d

Please sign in to comment.