Skip to content

Commit 0919b8b

Browse files
authored
Merge pull request #324 from librecaptcha/replace-jsoniter-zio-blocks-schema-1658979500912714944
Use ZIO Blocks Schema instead of jsoniter for JSON Parsing
2 parents ff14076 + a41edcf commit 0919b8b

13 files changed

Lines changed: 181 additions & 61 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ jobs:
1212

1313
steps:
1414
- uses: actions/checkout@v2
15-
- name: Set up JDK 1.11
15+
- name: Set up JDK 17
1616
uses: actions/setup-java@v1
1717
with:
18-
java-version: 1.11
18+
java-version: 17
1919
- uses: sbt/setup-sbt@v1
2020
- name: Run tests
2121
run: sbt test assembly

.github/workflows/docker-latest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Set up JDK
1818
uses: actions/setup-java@v1
1919
with:
20-
java-version: 1.16
20+
java-version: 17
2121

2222
- uses: sbt/setup-sbt@v1
2323

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
language: scala
2-
jdk: openjdk11
2+
jdk: openjdk17
33
scala:
44
- 2.13.2
55
script:

build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ lazy val root = (project in file(".")).settings(
1414
name := "LibreCaptcha",
1515
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-core" % "4.3.10",
1616
libraryDependencies += "com.sksamuel.scrimage" % "scrimage-filters" % "4.3.10",
17-
libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.38.9",
18-
libraryDependencies += "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.38.9" % "provided"
17+
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.31",
18+
1919
)
2020

2121
Compile / unmanagedResourceDirectories += { baseDirectory.value / "lib" }

src/main/scala/lc/core/captchaProviders.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import lc.captchas.interfaces.ChallengeProvider
55
import lc.captchas.interfaces.Challenge
66
import scala.collection.mutable.Map
77
import lc.misc.HelperFunctions
8+
import zio.blocks.schema.json.JsonFormat
9+
import java.nio.ByteBuffer
810

911
class CaptchaProviders(config: Config) {
1012
private val providers = Map(
@@ -30,13 +32,18 @@ class CaptchaProviders(config: Config) {
3032
}
3133

3234
private def filterProviderByParam(param: Parameters): Iterable[(String, String)] = {
35+
val codec = zio.blocks.schema.json.Json.schema.derive(JsonFormat.deriver)
36+
3337
val configFilter = for {
3438
configValue <- captchaConfig
3539
if configValue.allowedLevels.contains(param.level)
3640
if configValue.allowedMedia.contains(param.media)
3741
if configValue.allowedInputType.contains(param.input_type)
3842
if configValue.allowedSizes.contains(param.size)
39-
} yield (configValue.name, configValue.config.string)
43+
} yield {
44+
val strConfig = new String(BufferEncoder.encode(configValue.config, codec), "UTF-8")
45+
(configValue.name, strConfig)
46+
}
4047

4148
val providerFilter = for {
4249
providerValue <- configFilter

src/main/scala/lc/core/config.scala

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package lc.core
22

33
import scala.io.Source.fromFile
4-
import com.github.plokhotnyuk.jsoniter_scala.core._
4+
import zio.blocks.schema._
5+
import zio.blocks.schema.json._
56
import java.io.{FileNotFoundException, File, PrintWriter}
67
import java.{util => ju}
78
import lc.misc.HelperFunctions
9+
import java.nio.ByteBuffer
810

911
class Config(configFilePath: String) {
1012

@@ -33,7 +35,11 @@ class Config(configFilePath: String) {
3335
}
3436
}
3537

36-
private val appConfig = readFromString[AppConfig](configString)
38+
private val appConfigEither = AppConfig.codec.decode(ByteBuffer.wrap(configString.getBytes("UTF-8")))
39+
private val appConfig = appConfigEither match {
40+
case Right(conf) => conf
41+
case Left(err) => throw new Exception(err.toString)
42+
}
3743
private val configFields: ConfigField = appConfig.toConfigField
3844

3945
val port: Int = configFields.portInt.getOrElse(8888)
@@ -71,36 +77,36 @@ class Config(configFilePath: String) {
7177
allowedMedia = List("image/png"),
7278
allowedInputType = List("text"),
7379
allowedSizes = List("350x100"),
74-
config = JSONString("{}")
80+
config = Json.Object()
7581
),
7682
CaptchaConfig(
7783
name = "PoppingCharactersCaptcha",
7884
allowedLevels = List("hard"),
7985
allowedMedia = List("image/gif"),
8086
allowedInputType = List("text"),
8187
allowedSizes = List("350x100"),
82-
config = JSONString("{}")
88+
config = Json.Object()
8389
),
8490
CaptchaConfig(
8591
name = "ShadowTextCaptcha",
8692
allowedLevels = List("easy"),
8793
allowedMedia = List("image/png"),
8894
allowedInputType = List("text"),
8995
allowedSizes = List("350x100"),
90-
config = JSONString("{}")
96+
config = Json.Object()
9197
),
9298
CaptchaConfig(
9399
name = "RainDropsCaptcha",
94100
allowedLevels = List("easy", "medium"),
95101
allowedMedia = List("image/gif"),
96102
allowedInputType = List("text"),
97103
allowedSizes = List("350x100"),
98-
config = JSONString("{}")
104+
config = Json.Object()
99105
)
100106
)
101107
)
102108

103-
writeToString(defaultConfig, WriterConfig.withIndentionStep(2))
109+
new String(BufferEncoder.encode(defaultConfig, AppConfig.codec), "UTF-8")
104110
}
105111

106112
}

src/main/scala/lc/core/models.scala

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,88 @@
11
package lc.core
22

3-
import com.github.plokhotnyuk.jsoniter_scala.macros._
4-
import com.github.plokhotnyuk.jsoniter_scala.core._
3+
import zio.blocks.schema._
4+
import zio.blocks.schema.json._
5+
import zio.blocks.schema.codec.BinaryCodec
6+
import java.nio.ByteBuffer
57

68
trait ByteConvert { def toBytes(): Array[Byte] }
79
case class Size(height: Int, width: Int)
810

911
case class Parameters(level: String, media: String, input_type: String, size: String)
1012
object Parameters {
11-
implicit val codec: JsonValueCodec[Parameters] = JsonCodecMaker.make
13+
implicit val schema: Schema[Parameters] = Schema.derived
14+
implicit val codec: BinaryCodec[Parameters] = schema.derive(JsonFormat.deriver)
1215
}
1316

14-
case class Id(id: String) extends ByteConvert { def toBytes(): Array[Byte] = { writeToArray(this) } }
17+
object BufferEncoder {
18+
def encode[A](value: A, codec: BinaryCodec[A]): Array[Byte] = {
19+
// Start with 1KB, if it fails, try with 10KB, 100KB, etc up to 1MB
20+
var size = 1024
21+
var result: Array[Byte] = null
22+
while (result == null && size <= 1048576) {
23+
try {
24+
val buf = ByteBuffer.allocate(size)
25+
codec.encode(value, buf)
26+
buf.flip()
27+
result = new Array[Byte](buf.remaining())
28+
buf.get(result)
29+
} catch {
30+
case _: java.nio.BufferOverflowException =>
31+
size *= 10
32+
}
33+
}
34+
if (result == null) {
35+
throw new Exception("Buffer overflow encoding object")
36+
}
37+
result
38+
}
39+
}
40+
41+
case class Id(id: String) extends ByteConvert {
42+
def toBytes(): Array[Byte] = {
43+
BufferEncoder.encode(this, Id.codec)
44+
}
45+
}
1546
object Id {
16-
implicit val codec: JsonValueCodec[Id] = JsonCodecMaker.make
47+
implicit val schema: Schema[Id] = Schema.derived
48+
implicit val codec: BinaryCodec[Id] = schema.derive(JsonFormat.deriver)
1749
}
1850

1951
case class Image(image: Array[Byte]) extends ByteConvert { def toBytes(): Array[Byte] = { image } }
2052

2153
case class Answer(answer: String, id: String)
2254
object Answer {
23-
implicit val codec: JsonValueCodec[Answer] = JsonCodecMaker.make
55+
implicit val schema: Schema[Answer] = Schema.derived
56+
implicit val codec: BinaryCodec[Answer] = schema.derive(JsonFormat.deriver)
2457
}
2558

26-
case class Success(result: String) extends ByteConvert { def toBytes(): Array[Byte] = { writeToArray(this) } }
27-
object Success {
28-
implicit val codec: JsonValueCodec[Success] = JsonCodecMaker.make
59+
case class Success(result: String) extends ByteConvert {
60+
def toBytes(): Array[Byte] = {
61+
BufferEncoder.encode(this, Success.codec)
62+
}
2963
}
30-
31-
case class Error(message: String) extends ByteConvert { def toBytes(): Array[Byte] = { writeToArray(this) } }
32-
object Error {
33-
implicit val codec: JsonValueCodec[Error] = JsonCodecMaker.make
64+
object Success {
65+
implicit val schema: Schema[Success] = Schema.derived
66+
implicit val codec: BinaryCodec[Success] = schema.derive(JsonFormat.deriver)
3467
}
3568

36-
case class JSONString(string: String)
37-
38-
object JSONString {
39-
implicit val codec: JsonValueCodec[JSONString] = new JsonValueCodec[JSONString] {
40-
def decodeValue(in: JsonReader, default: JSONString): JSONString = {
41-
val raw = in.readRawValAsBytes()
42-
JSONString(new String(raw, "UTF-8"))
43-
}
44-
45-
def encodeValue(x: JSONString, out: JsonWriter): Unit = {
46-
out.writeRawVal(x.string.getBytes("UTF-8"))
47-
}
48-
49-
def nullValue: JSONString = null.asInstanceOf[JSONString]
69+
case class Error(message: String) extends ByteConvert {
70+
def toBytes(): Array[Byte] = {
71+
BufferEncoder.encode(this, Error.codec)
5072
}
5173
}
74+
object Error {
75+
implicit val schema: Schema[Error] = Schema.derived
76+
implicit val codec: BinaryCodec[Error] = schema.derive(JsonFormat.deriver)
77+
}
5278

5379
case class CaptchaConfig(
5480
name: String,
5581
allowedLevels: List[String],
5682
allowedMedia: List[String],
5783
allowedInputType: List[String],
5884
allowedSizes: List[String],
59-
config: JSONString
85+
config: zio.blocks.schema.json.Json.Object
6086
)
6187

6288
case class AppConfig(
@@ -77,7 +103,8 @@ case class AppConfig(
77103
)
78104
}
79105
object AppConfig {
80-
implicit val codec: JsonValueCodec[AppConfig] = JsonCodecMaker.make
106+
implicit val schema: Schema[AppConfig] = Schema.derived
107+
implicit val codec: BinaryCodec[AppConfig] = schema.derive(JsonFormat.deriver)
81108
}
82109
case class ConfigField(
83110
port: Option[Int] = None,

src/main/scala/lc/server/Server.scala

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package lc.server
22

3-
import com.github.plokhotnyuk.jsoniter_scala.core._
3+
import zio.blocks.schema._
4+
import zio.blocks.schema.json._
45
import lc.core.CaptchaManager
56
import lc.core.ErrorMessageEnum
67
import lc.core.{Answer, ByteConvert, Error, Id, Parameters}
@@ -9,6 +10,7 @@ import org.limium.picoserve.Server.{ByteResponse, ServerBuilder, StringResponse}
910
import scala.io.Source
1011
import java.net.InetSocketAddress
1112
import java.util
13+
import java.nio.ByteBuffer
1214
import scala.jdk.CollectionConverters._
1315

1416
class Server(
@@ -18,7 +20,7 @@ class Server(
1820
playgroundEnabled: Boolean,
1921
corsHeader: String
2022
) {
21-
var headerMap: util.Map[String, util.List[String]] = _
23+
var headerMap: util.Map[String, util.List[String]] = null
2224
if (corsHeader.nonEmpty) {
2325
headerMap = Map("Access-Control-Allow-Origin" -> List(corsHeader).asJava).asJava
2426
}
@@ -29,9 +31,15 @@ class Server(
2931
.POST(
3032
"/v2/captcha",
3133
(request) => {
32-
val param = readFromString[Parameters](request.getBodyString())
33-
val id = captchaManager.getChallenge(param)
34-
getResponse(id, headerMap)
34+
val bodyStr = request.getBodyString().trim.replaceAll("\u0000", "")
35+
val paramEither = Parameters.codec.decode(ByteBuffer.wrap(bodyStr.getBytes("UTF-8")))
36+
paramEither match {
37+
case Right(param) =>
38+
val id = captchaManager.getChallenge(param)
39+
getResponse(id, headerMap)
40+
case Left(err) =>
41+
getResponse(Left(Error("Invalid parameters: " + err.toString)), headerMap)
42+
}
3543
}
3644
)
3745
.GET(
@@ -51,18 +59,25 @@ class Server(
5159
.POST(
5260
"/v2/answer",
5361
(request) => {
54-
val answer = readFromString[Answer](request.getBodyString())
55-
val result = captchaManager.checkAnswer(answer)
56-
getResponse(result, headerMap)
62+
val bodyStr = request.getBodyString().trim.replaceAll("\u0000", "")
63+
val answerEither = Answer.codec.decode(ByteBuffer.wrap(bodyStr.getBytes("UTF-8")))
64+
answerEither match {
65+
case Right(answer) =>
66+
val result = captchaManager.checkAnswer(answer)
67+
getResponse(result, headerMap)
68+
case Left(err) =>
69+
getResponse(Left(Error("Invalid answer format: " + err.toString)), headerMap)
70+
}
5771
}
5872
)
5973
if (playgroundEnabled) {
74+
val htmlHeaderMap = Map("Content-Type" -> List("text/html").asJava).asJava
6075
serverBuilder.GET(
6176
"/demo/index.html",
6277
(_) => {
6378
val resStream = getClass().getResourceAsStream("/index.html")
6479
val str = Source.fromInputStream(resStream).mkString
65-
new StringResponse(200, str)
80+
new StringResponse(200, str, htmlHeaderMap)
6681
}
6782
)
6883
serverBuilder.GET(
@@ -74,8 +89,8 @@ class Server(
7489
<h3><a href="/demo/index.html">Link to Demo</a></h3>
7590
<h3>API is served at <b>/v2/</b></h3>
7691
</html>
77-
"""
78-
new StringResponse(200, str)
92+
""";
93+
new StringResponse(200, str, htmlHeaderMap)
7994
}
8095
)
8196
println("Playground enabled on /demo/index.html")

0 commit comments

Comments
 (0)