diff --git a/play-json/jvm/src/main/scala/play/api/libs/json/BigDecimalParser.scala b/play-json/jvm/src/main/scala/play/api/libs/json/BigDecimalParser.scala index 7c04fd5b..581326e0 100644 --- a/play-json/jvm/src/main/scala/play/api/libs/json/BigDecimalParser.scala +++ b/play-json/jvm/src/main/scala/play/api/libs/json/BigDecimalParser.scala @@ -4,23 +4,34 @@ package play.api.libs.json +import java.math.{ BigDecimal => JBigDecimal } + private[json] object BigDecimalParser { - def parse(input: String, jsonConfig: JsonConfig): JsResult[java.math.BigDecimal] = { + def parse(input: String, jsonConfig: JsonConfig): JsResult[JBigDecimal] = { + import jsonConfig.bigDecimalParseConfig // There is a limit of how large the numbers can be since parsing extremely // large numbers (think thousand of digits) and operating on the parsed values // can potentially cause a DDoS. - if (input.length > jsonConfig.bigDecimalParseConfig.digitsLimit) { + if (input.length > bigDecimalParseConfig.digitsLimit) { JsError("error.expected.numberdigitlimit") } else { // Must create the BigDecimal with a MathContext that is consistent with the limits used. try { - val bigDecimal = new java.math.BigDecimal(input, jsonConfig.bigDecimalParseConfig.mathContext) + val bigDecimal: JBigDecimal = { + if (bigDecimalParseConfig.useJacksonParser) { + com.fasterxml.jackson.core.io.NumberInput.parseBigDecimal(input, bigDecimalParseConfig.useJacksonFastParser) + + } else { + new JBigDecimal(input, bigDecimalParseConfig.mathContext) + } + } // We should also avoid numbers with scale that are out of a safe limit val scale = bigDecimal.scale - if (Math.abs(scale) > jsonConfig.bigDecimalParseConfig.scaleLimit) { + + if (Math.abs(scale) > bigDecimalParseConfig.scaleLimit) { JsError(JsonValidationError("error.expected.numberscalelimit", scale)) } else { JsSuccess(bigDecimal) 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 38f999c9..71574048 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,15 +4,16 @@ package play.api.libs.json -import com.fasterxml.jackson.core.StreamReadConstraints -import com.fasterxml.jackson.core.StreamWriteConstraints - -import play.api.libs.json.JsonConfig.defaultMaxPlain -import play.api.libs.json.JsonConfig.defaultMinPlain -import play.api.libs.json.JsonConfig.defaultDigitsLimit -import play.api.libs.json.JsonConfig.defaultMathContext -import play.api.libs.json.JsonConfig.defaultPreserveZeroDecimal -import play.api.libs.json.JsonConfig.defaultScaleLimit +import com.fasterxml.jackson.core.{ StreamReadConstraints, StreamWriteConstraints } + +import play.api.libs.json.JsonConfig.{ + defaultMaxPlain, + defaultMinPlain, + defaultDigitsLimit, + defaultMathContext, + defaultPreserveZeroDecimal, + defaultScaleLimit +} import java.math.MathContext @@ -44,18 +45,57 @@ sealed trait BigDecimalParseConfig { * This can be set using the [[JsonConfig.digitsLimitProperty]] system property. */ def digitsLimit: Int + + /** + * Jackson has its own parser for `BigDecimal` + * and this can be further configured to use a fast parser. + * This can be set using the [[JsonConfig.useJacksonParserProperty]] system property. + */ + def useJacksonParser: Boolean + + /** + * Only used when `useJacksonBigDecimalParser` is true. + * This can be set using the [[JsonConfig.useJacksonBigDecimalFastParserProperty]] + * system property. + */ + def useJacksonFastParser: Boolean } object BigDecimalParseConfig { + def apply( mathContext: MathContext = defaultMathContext, scaleLimit: Int = defaultScaleLimit, - digitsLimit: Int = defaultDigitsLimit - ): BigDecimalParseConfig = BigDecimalParseConfigImpl(mathContext, scaleLimit, digitsLimit) + digitsLimit: Int = defaultDigitsLimit, + useJacksonParser: Boolean = false, + useJacksonFastParser: Boolean = false + ): BigDecimalParseConfig = BigDecimalParseConfigImpl( + mathContext, + scaleLimit, + digitsLimit, + useJacksonParser, + useJacksonFastParser + ) } -private final case class BigDecimalParseConfigImpl(mathContext: MathContext, scaleLimit: Int, digitsLimit: Int) - extends BigDecimalParseConfig +private final case class BigDecimalParseConfigImpl( + mathContext: MathContext, + scaleLimit: Int, + digitsLimit: Int, + useJacksonParser: Boolean, + useJacksonFastParser: Boolean +) extends BigDecimalParseConfig + +private object BigDecimalParseConfigImpl + extends scala.runtime.AbstractFunction3[MathContext, Int, Int, BigDecimalParseConfig] { + + @deprecated("Specify useJacksonParser and useJacksonFastParser parameters", "") + def apply( + mathContext: MathContext, + scaleLimit: Int, + digitsLimit: Int, + ): BigDecimalParseConfig = BigDecimalParseConfigImpl(mathContext, scaleLimit, digitsLimit, false, false) +} sealed trait BigDecimalSerializerConfig { @@ -152,6 +192,18 @@ object JsonConfig { */ val digitsLimitProperty: String = "play.json.parser.digitsLimit" + /** + * The system property to toggle the Jackson BigDecimal parser + */ + val useJacksonBigDecimalParserProperty: String = + "play.json.parser.useJacksonBigDecimalParser" + + /** + * The system property to toggle the fast-mode for the Jackson BigDecimal parser + */ + val useJacksonBigDecimalFastParserProperty: String = + "play.json.parser.useJacksonBigDecimalFastParser" + /** * The system property to override the math context. This can be "decimal32", "decimal64", "decimal128" (the default), * or "unlimited". @@ -195,6 +247,12 @@ object JsonConfig { private[json] def loadMathContext: MathContext = parseMathContext(mathContextProperty) + private[json] def loadUseJacksonBigDecimalParser: Boolean = + prop(useJacksonBigDecimalParserProperty, false)(_.toBoolean) + + private[json] def loadUseJacksonBigDecimalFastParser: Boolean = + prop(useJacksonBigDecimalFastParserProperty, false)(_.toBoolean) + private[json] def loadMinPlain: BigDecimal = prop(minPlainProperty, defaultMinPlain)(BigDecimal.exact) private[json] def loadMaxPlain: BigDecimal = prop(maxPlainProperty, defaultMaxPlain)(BigDecimal.exact) @@ -229,7 +287,13 @@ object JsonConfig { // To override, call JacksonJson.setConfig() val settings: JsonConfig = JsonConfig( - BigDecimalParseConfig(loadMathContext, loadScaleLimit, loadDigitsLimit), + BigDecimalParseConfig( + loadMathContext, + loadScaleLimit, + loadDigitsLimit, + loadUseJacksonBigDecimalParser, + loadUseJacksonBigDecimalFastParser + ), BigDecimalSerializerConfig(loadMinPlain, loadMaxPlain, loadPreserveZeroDecimal), defaultStreamReadConstraints, defaultStreamWriteConstraints diff --git a/play-json/jvm/src/test/scala/play/api/libs/json/JsonConfigSpec.scala b/play-json/jvm/src/test/scala/play/api/libs/json/JsonConfigSpec.scala index cfd0c407..5892994d 100644 --- a/play-json/jvm/src/test/scala/play/api/libs/json/JsonConfigSpec.scala +++ b/play-json/jvm/src/test/scala/play/api/libs/json/JsonConfigSpec.scala @@ -5,32 +5,62 @@ package play.api.libs.json import com.fasterxml.jackson.core.{ StreamReadConstraints, StreamWriteConstraints } + import org.scalatest.matchers.must.Matchers import org.scalatest.wordspec.AnyWordSpec -class JsonConfigSpec extends AnyWordSpec with Matchers { +final class JsonConfigSpec extends AnyWordSpec with Matchers { "JsonConfig" should { - "fetch default nesting depth (parsing)" in { - JsonConfig.defaultStreamReadConstraints.getMaxNestingDepth.mustEqual(StreamReadConstraints.DEFAULT_MAX_DEPTH) + "fetch default setting".which { + "is defined for nesting depth (parsing)" in { + JsonConfig.defaultStreamReadConstraints.getMaxNestingDepth.mustEqual(StreamReadConstraints.DEFAULT_MAX_DEPTH) + } + + "is defined for nesting depth (serializer)" in { + JsonConfig.defaultStreamWriteConstraints.getMaxNestingDepth.mustEqual(StreamWriteConstraints.DEFAULT_MAX_DEPTH) + } + + "is defined for Jackson BigDecimal parser" in { + JsonConfig.loadUseJacksonBigDecimalParser.mustEqual(false) + + JsonConfig.settings.bigDecimalParseConfig.useJacksonParser.mustEqual(false) + + JsonConfig.loadUseJacksonBigDecimalFastParser.mustEqual(false) + + JsonConfig.settings.bigDecimalParseConfig.useJacksonFastParser.mustEqual(false) + } } - "fetch default nesting depth (serializer)" in { - JsonConfig.defaultStreamWriteConstraints.getMaxNestingDepth.mustEqual(StreamWriteConstraints.DEFAULT_MAX_DEPTH) + + "override nesting depth (parsing)" in withProperty(JsonConfig.maxNestingDepth, "200") { + JsonConfig.loadMaxNestingDepth.mustEqual(200) } - "override nesting depth (parsing)" in { - System.setProperty(JsonConfig.maxNestingDepth, "200") - try { - JsonConfig.loadMaxNestingDepth.mustEqual(200) - } finally { - System.clearProperty(JsonConfig.maxNestingDepth) - } + + "override nesting depth (serializer)" in withProperty(JsonConfig.maxSerializerNestingDepth, "300") { + + JsonConfig.loadMaxSerializerNestingDepth.mustEqual(300) } - "override nesting depth (serializer)" in { - System.setProperty(JsonConfig.maxSerializerNestingDepth, "300") - try { - JsonConfig.loadMaxSerializerNestingDepth.mustEqual(300) - } finally { - System.clearProperty(JsonConfig.maxSerializerNestingDepth) - } + + "override use of Jackson BigDecimal parser" in withProperty("play.json.parser.useJacksonBigDecimalParser", "true") { + JsonConfig.loadUseJacksonBigDecimalParser.mustEqual(true) + } + + "override the use of Jackson BigDecimal fast parser" in withProperty( + "play.json.parser.useJacksonBigDecimalFastParser", + "true" + ) { + JsonConfig.loadUseJacksonBigDecimalFastParser.mustEqual(true) + } + } + + // --- + + private def withProperty[T](key: String, value: String)(f: => T): T = { + System.setProperty(key, value) + + try { + f + } finally { + System.clearProperty(key) } } }