diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 3490cc6da..f23d3552d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,6 +1,7 @@ name: Check on: + workflow_dispatch: pull_request: push: @@ -19,9 +20,9 @@ jobs: with: cmd: sbt validateCode - check-binary-compatibility: - name: Binary Compatibility - uses: playframework/.github/.github/workflows/binary-check.yml@v4 + #check-binary-compatibility: + # name: Binary Compatibility + # uses: playframework/.github/.github/workflows/binary-check.yml@v4 check-docs: name: Docs @@ -33,11 +34,11 @@ jobs: name: Tests needs: - "check-code-style" - - "check-binary-compatibility" - "check-docs" + # - "check-binary-compatibility" uses: playframework/.github/.github/workflows/cmd.yml@v4 with: - java: 21, 17 + java: 21, 17, 11 scala: 2.12.x, 2.13.x, 3.x cmd: scripts/test-code.sh diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml deleted file mode 100644 index a9f76d5da..000000000 --- a/.github/workflows/release-drafter.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Release Drafter - -on: - push: - branches: - - main - -jobs: - update_release_draft: - runs-on: ubuntu-latest - steps: - - uses: release-drafter/release-drafter@v6 - with: - name: "Play JSON $RESOLVED_VERSION" - config-name: release-drafts/increasing-minor-version.yml # located in .github/ in the default branch within this or the .github repo - commitish: ${{ github.ref_name }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/build.sbt b/build.sbt index 223fcb45a..04da9357b 100644 --- a/build.sbt +++ b/build.sbt @@ -21,12 +21,12 @@ def specs2(scalaVersion: String) = ("org.specs2" %% s"specs2-$n" % "4.20.9") % Test } -val jacksonDatabindVersion = "2.14.3" +val jacksonDatabindVersion = "2.17.3" val jacksonDatabind = Seq( "com.fasterxml.jackson.core" % "jackson-databind" % jacksonDatabindVersion ) -val jacksonVersion = "2.14.3" +val jacksonVersion = jacksonDatabindVersion val jacksons = Seq( "com.fasterxml.jackson.core" % "jackson-core", "com.fasterxml.jackson.core" % "jackson-annotations", @@ -65,7 +65,7 @@ def playJsonMimaSettings = Seq( val javacSettings = Seq( "-source", - "17", + "11", "-Xlint:deprecation", "-Xlint:unchecked", ) @@ -73,7 +73,7 @@ val javacSettings = Seq( val scalacOpts = Seq( "-language:higherKinds", "-release", - "17", + "11", "-Ywarn-unused:imports", "-Xlint:nullary-unit", "-Xlint", @@ -110,7 +110,7 @@ lazy val commonSettings = Def.settings( crossScalaVersions := Seq(Dependencies.Scala212, Dependencies.Scala213, Dependencies.Scala3), Compile / javacOptions ++= javacSettings, Test / javacOptions ++= javacSettings, - Compile / compile / javacOptions ++= Seq("--release", "17"), // sbt #1785, avoids passing to javadoc + Compile / compile / javacOptions ++= Seq("--release", "11"), // sbt #1785, avoids passing to javadoc scalacOptions ++= (if (isScala3.value) Nil else scalacOpts), Compile / doc / scalacOptions ++= Seq( // Work around 2.12 bug which prevents javadoc in nested java classes from compiling. @@ -124,22 +124,17 @@ lazy val root = project .enablePlugins(ScalaJSPlugin) .disablePlugins(MimaPlugin) .aggregate( - `play-jsonJS`, - `play-jsonJVM`, - `play-jsonNative`, - `play-functionalJS`, - `play-functionalJVM`, - `play-functionalNative`, - `play-json-joda` + `play-jsonJVM` ) .settings(commonSettings) .settings(publish / skip := true) -lazy val `play-json` = crossProject(JVMPlatform, JSPlatform, NativePlatform) +lazy val `play-json` = crossProject(JVMPlatform) .crossType(CrossType.Full) .in(file("play-json")) .enablePlugins(Omnidoc, Playdoc) .configs(Docs) + /* .jsSettings( libraryDependencies ++= Seq( ("org.scala-js" %%% "scalajs-java-securerandom" % "1.0.0").cross(CrossVersion.for3Use2_13), @@ -150,6 +145,7 @@ lazy val `play-json` = crossProject(JVMPlatform, JSPlatform, NativePlatform) "org.typelevel" %%% "jawn-parser" % "1.6.0" ) ) + */ .settings( commonSettings ++ playJsonMimaSettings ++ Def.settings( libraryDependencies ++= ( @@ -237,10 +233,10 @@ lazy val `play-json` = crossProject(JVMPlatform, JSPlatform, NativePlatform) }.taskValue ) ) - .dependsOn(`play-functional`) +//.dependsOn(`play-functional`) -lazy val `play-jsonJS` = `play-json`.js -lazy val `play-jsonNative` = `play-json`.native +// lazy val `play-jsonJS` = `play-json`.js +// lazy val `play-jsonNative` = `play-json`.native lazy val `play-jsonJVM` = `play-json`.jvm .settings( @@ -250,8 +246,10 @@ lazy val `play-jsonJVM` = `play-json`.jvm specs2(scalaVersion.value).map(_.exclude("org.scala-lang.modules", "scala-xml_2.13")) else specs2(scalaVersion.value) - } :+ ( - "ch.qos.logback" % "logback-classic" % "1.5.12" % Test + } ++ Seq( + "org.playframework" %% "play-functional" % "3.0.4", + "com.typesafe" % "config" % "1.4.3", + "ch.qos.logback" % "logback-classic" % "1.3.15" % Test ), Test / unmanagedSourceDirectories ++= (docsP / PlayDocsKeys.scalaManualSourceDirectories).value, ) @@ -263,6 +261,7 @@ def enableJol = Seq( compileOrder := CompileOrder.JavaThenScala, ) +/* lazy val `play-json-joda` = project .in(file("play-json-joda")) .enablePlugins(Omnidoc) @@ -284,6 +283,7 @@ lazy val `play-functional` = crossProject(JVMPlatform, JSPlatform, NativePlatfor lazy val `play-functionalJVM` = `play-functional`.jvm lazy val `play-functionalJS` = `play-functional`.js lazy val `play-functionalNative` = `play-functional`.native + */ lazy val benchmarks = project .in(file("benchmarks")) diff --git a/play-json/jvm/src/main/mima-filters/3.1.x.backwards.excludes/PR1072.backwards.excludes b/play-json/jvm/src/main/mima-filters/3.1.x.backwards.excludes/PR1072.backwards.excludes new file mode 100644 index 000000000..b417d126f --- /dev/null +++ b/play-json/jvm/src/main/mima-filters/3.1.x.backwards.excludes/PR1072.backwards.excludes @@ -0,0 +1,2 @@ +# private final class JsonConfigImpl has changed +ProblemFilters.exclude[MissingTypesProblem]("play.api.libs.json.JsonConfigImpl$") diff --git a/play-json/jvm/src/main/resources/reference.conf b/play-json/jvm/src/main/resources/reference.conf new file mode 100644 index 000000000..e0c6ffce3 --- /dev/null +++ b/play-json/jvm/src/main/resources/reference.conf @@ -0,0 +1,10 @@ +play.json { + jackson { + read { + # see https://www.javadoc.io/static/com.fasterxml.jackson.core/jackson-core/2.17.3/com/fasterxml/jackson/core/StreamReadConstraints.html + # these defaults are the same as the defaults in `StreamReadConstraints` + max-nesting-depth = 1000 + max-string-length = 20000000 + } + } +} diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala b/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala index 71d21d963..105c49c7b 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala @@ -4,6 +4,9 @@ package play.api.libs.json +import com.fasterxml.jackson.core.StreamReadConstraints +import com.typesafe.config.ConfigFactory + import play.api.libs.json.JsonConfig.defaultMaxPlain import play.api.libs.json.JsonConfig.defaultMinPlain import play.api.libs.json.JsonConfig.defaultDigitsLimit @@ -103,6 +106,7 @@ private final case class DecimalSerializerSettingsImpl( sealed trait JsonConfig { def bigDecimalParseConfig: BigDecimalParseConfig def bigDecimalSerializerConfig: BigDecimalSerializerConfig + def streamReadConstraints: StreamReadConstraints } object JsonConfig { @@ -168,11 +172,26 @@ object JsonConfig { */ val maxPlainProperty: String = "play.json.serializer.maxPlain" + /** + * The system property to override the max nesting depth for JSON parsing. + */ + val maxNestingDepth: String = "play.json.parser.maxNestingDepth" + + /** + * The system property to override the max string length for JSON parsing. + * This is used to limit the length of individual strings in JSON documents. + */ + val maxStringLength: String = "play.json.parser.maxStringLength" + /** * The system property to override whether zero decimals (e.g. .0 or .00) are written by default. These are dropped by default. */ val preserveZeroDecimalProperty: String = "play.json.serializer.preserveZeroDecimal" + private val playJsonConfig = ConfigFactory + .load() + .getConfig("play.json.jackson") + private[json] def loadScaleLimit: Int = prop(scaleLimitProperty, defaultScaleLimit)(_.toInt) private[json] def loadDigitsLimit: Int = prop(digitsLimitProperty, defaultDigitsLimit)(_.toInt) @@ -183,15 +202,30 @@ object JsonConfig { private[json] def loadMaxPlain: BigDecimal = prop(maxPlainProperty, defaultMaxPlain)(BigDecimal.exact) + private[json] def loadMaxNestingDepth: Int = + prop(maxNestingDepth, playJsonConfig.getInt("read.max-nesting-depth"))(Integer.parseInt) + + private[json] def loadMaxStringLength: Int = + prop(maxStringLength, playJsonConfig.getInt("read.max-string-length"))(Integer.parseInt) + private[json] def loadPreserveZeroDecimal: Boolean = prop(preserveZeroDecimalProperty, defaultPreserveZeroDecimal)(_.toBoolean) + private[json] val defaultStreamReadConstraints: StreamReadConstraints = + StreamReadConstraints + .builder() + .maxNestingDepth(loadMaxNestingDepth) + .maxStringLength(loadMaxStringLength) + .maxNumberLength(Int.MaxValue) // play-json has its own support for limiting number length + .build() + // Default settings, which can be controlled with system properties. // To override, call JacksonJson.setConfig() val settings: JsonConfig = JsonConfig( BigDecimalParseConfig(loadMathContext, loadScaleLimit, loadDigitsLimit), - BigDecimalSerializerConfig(loadMinPlain, loadMaxPlain, loadPreserveZeroDecimal) + BigDecimalSerializerConfig(loadMinPlain, loadMaxPlain, loadPreserveZeroDecimal), + defaultStreamReadConstraints ) def apply(): JsonConfig = apply(BigDecimalParseConfig(), BigDecimalSerializerConfig()) @@ -200,7 +234,14 @@ object JsonConfig { bigDecimalParseConfig: BigDecimalParseConfig, bigDecimalSerializerConfig: BigDecimalSerializerConfig ): JsonConfig = - JsonConfigImpl(bigDecimalParseConfig, bigDecimalSerializerConfig) + JsonConfigImpl(bigDecimalParseConfig, bigDecimalSerializerConfig, defaultStreamReadConstraints) + + def apply( + bigDecimalParseConfig: BigDecimalParseConfig, + bigDecimalSerializerConfig: BigDecimalSerializerConfig, + streamReadConstraints: StreamReadConstraints + ): JsonConfig = + JsonConfigImpl(bigDecimalParseConfig, bigDecimalSerializerConfig, streamReadConstraints) private[json] def parseMathContext(key: String): MathContext = sys.props.get(key).map(_.toLowerCase) match { case Some("decimal128") => MathContext.DECIMAL128 @@ -220,7 +261,8 @@ object JsonConfig { private final case class JsonConfigImpl( bigDecimalParseConfig: BigDecimalParseConfig, - bigDecimalSerializerConfig: BigDecimalSerializerConfig + bigDecimalSerializerConfig: BigDecimalSerializerConfig, + streamReadConstraints: StreamReadConstraints ) extends JsonConfig @deprecated("Use BigDecimalParseConfig instead", "2.9.4") @@ -241,7 +283,8 @@ final case class BigDecimalSerializerSettings( @deprecated("Use JsonConfig instead", "2.9.4") final case class JsonParserSettings( bigDecimalParseSettings: BigDecimalParseSettings, - bigDecimalSerializerSettings: BigDecimalSerializerSettings + bigDecimalSerializerSettings: BigDecimalSerializerSettings, + streamReadConstraints: StreamReadConstraints = JsonConfig.defaultStreamReadConstraints ) extends JsonConfig { override def bigDecimalParseConfig: BigDecimalParseConfig = bigDecimalParseSettings diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala b/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala index 44f0c090d..261f0374d 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/jackson/JacksonJson.scala @@ -6,6 +6,7 @@ package play.api.libs.json.jackson import java.io.InputStream import java.io.StringWriter +import java.math.BigInteger import scala.annotation.switch import scala.annotation.tailrec @@ -13,11 +14,7 @@ import scala.collection.mutable import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ListBuffer -import com.fasterxml.jackson.core.JsonFactory -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.core.JsonTokenId -import com.fasterxml.jackson.core.Version +import com.fasterxml.jackson.core.{ JsonFactoryBuilder, JsonGenerator, JsonParser, JsonTokenId, Version } import com.fasterxml.jackson.core.json.JsonWriteFeature import com.fasterxml.jackson.core.util.DefaultPrettyPrinter @@ -25,8 +22,11 @@ import com.fasterxml.jackson.databind.Module.SetupContext import com.fasterxml.jackson.databind._ import com.fasterxml.jackson.databind.`type`.TypeFactory import com.fasterxml.jackson.databind.deser.Deserializers +import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.module.SimpleModule +import com.fasterxml.jackson.databind.node.BigIntegerNode import com.fasterxml.jackson.databind.ser.Serializers +import com.fasterxml.jackson.databind.util.TokenBuffer import play.api.libs.json._ @@ -67,12 +67,8 @@ sealed class PlayJsonMapperModule(jsonConfig: JsonConfig) extends SimpleModule(" // -- Serializers. private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSerializer[JsValue] { - import java.math.BigInteger import java.math.{ BigDecimal => JBigDec } - import com.fasterxml.jackson.databind.node.BigIntegerNode - import com.fasterxml.jackson.databind.node.DecimalNode - private def stripTrailingZeros(bigDec: JBigDec): JBigDec = { val stripped = bigDec.stripTrailingZeros if (jsonConfig.bigDecimalSerializerConfig.preserveZeroDecimal && bigDec.scale > 0 && stripped.scale <= 0) { @@ -96,10 +92,15 @@ private[jackson] class JsValueSerializer(jsonConfig: JsonConfig) extends JsonSer val stripped = stripTrailingZeros(v.bigDecimal) val raw = if (shouldWritePlain) stripped.toPlainString else stripped.toString - if (raw.indexOf('E') < 0 && raw.indexOf('.') < 0) - json.writeTree(new BigIntegerNode(new BigInteger(raw))) + if (raw.exists(c => c == 'E' || c == '.')) + json.writeNumber(raw) else - json.writeTree(new DecimalNode(new JBigDec(raw))) + json match { + case _: TokenBuffer => + json.writeTree(new BigIntegerNode(new BigInteger(raw))) + case _ => + json.writeNumber(raw) + } } case JsString(v) => json.writeString(v) @@ -219,7 +220,7 @@ private[jackson] class JsValueDeserializer(factory: TypeFactory, klass: Class[_] case JsonTokenId.ID_FIELD_NAME => parserContext match { - case (c: ReadingMap) :: stack => (None, c.setField(jp.getCurrentName) +: stack) + case (c: ReadingMap) :: stack => (None, c.setField(jp.currentName()) +: stack) case _ => throw new RuntimeException("We should be reading map, something got wrong") } @@ -282,9 +283,13 @@ private[json] object JacksonJson { } private[json] case class JacksonJson(jsonConfig: JsonConfig) { - private val mapper = (new ObjectMapper).registerModule(new PlayJsonMapperModule(jsonConfig)) - - private val jsonFactory = new JsonFactory(mapper) + private val jsonFactory = new JsonFactoryBuilder() + .streamReadConstraints(jsonConfig.streamReadConstraints) + .build() + private val mapper = JsonMapper + .builder(jsonFactory) + .addModule(new PlayJsonMapperModule(jsonConfig)) + .build() private def stringJsonGenerator(out: java.io.StringWriter) = jsonFactory.createGenerator(out) diff --git a/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala b/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala index 8411c61ea..03bf83184 100644 --- a/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala +++ b/play-json/jvm/src/test/scala/play/api/libs/json/JsonSpec.scala @@ -4,6 +4,7 @@ package play.api.libs.json +import java.math.BigInteger import java.util.Calendar import java.util.Date import java.util.TimeZone @@ -477,6 +478,55 @@ class JsonSpec extends org.specs2.mutable.Specification { fromJson[JsonNode](jsNum).map(_.toString).must_==(JsSuccess("12.345")) } + "Serialize JsNumbers with integers correctly" in { + val numStrings = Seq( + "0", + "1", + "-1", + Int.MaxValue.toString, + Int.MinValue.toString, + Long.MaxValue.toString, + Long.MinValue.toString, + BigInteger.valueOf(Long.MaxValue).add(BigInteger.ONE).toString, + BigInteger.valueOf(Long.MinValue).add(BigInteger.valueOf(-1)).toString + ) + numStrings.map { numString => + val bigDec = new java.math.BigDecimal(numString) + Json.stringify(JsNumber(bigDec)).must_==(bigDec.toString) + } + } + + "Serialize JsNumbers with decimal points correctly" in { + val numStrings = Seq( + "0.123", + "1.23456789", + "-1.23456789", + Float.MaxValue.toString, + Float.MinValue.toString, + Double.MaxValue.toString, + Double.MinValue.toString, + java.math.BigDecimal.valueOf(Double.MaxValue).add(java.math.BigDecimal.valueOf(1)).toString, + java.math.BigDecimal.valueOf(Double.MinValue).add(java.math.BigDecimal.valueOf(-1)).toString + ) + numStrings.map { numString => + val bigDec = new java.math.BigDecimal(numString) + Json.stringify(JsNumber(bigDec)).must_==(bigDec.toString) + } + } + + "Serialize JsNumbers with e notation correctly" in { + val numStrings = Seq( + "1.23456789012345679012345679e999", + "-1.23456789012345679012345679e999", + "1.23456789012345679012345679e-999", + "-1.23456789012345679012345679e-999" + ) + numStrings.map { numString => + val bigDec = new java.math.BigDecimal(numString) + Json.stringify(JsNumber(bigDec)).must_==(bigDec.toString) + } + } + "parse from InputStream" in { val js = Json.obj( "key1" -> "value1", diff --git a/project/Common.scala b/project/Common.scala index c1afdc084..e1bafa44e 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -11,10 +11,10 @@ object Common extends AutoPlugin { override def globalSettings = Seq( - organization := "org.playframework", - organizationName := "The Play Framework Project", - organizationHomepage := Some(url("https://playframework.com/")), - homepage := Some(url(s"https://github.com/playframework/${repoName}")), + organization := "com.github.pjfanning", + organizationName := "com.github.pjfanning", + organizationHomepage := Some(url("https://github.com/pjfanning")), + homepage := Some(url(s"https://github.com/pjfanning/${repoName}")), licenses := Seq("Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0.html")), scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-encoding", "utf8"), javacOptions ++= Seq("-encoding", "UTF-8", "-Xlint:-options"), @@ -24,6 +24,12 @@ object Common extends AutoPlugin { "contact@playframework.com", url("https://github.com/playframework") ), - description := "Play JSON" + developers += Developer( + "pjfanning", + "PJ Fanning", + "", + url("https://github.com/pjfanning") + ), + description := "Play JSON (fork)" ) } diff --git a/project/build.properties b/project/build.properties index db1723b08..73df629ac 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.5 +sbt.version=1.10.7