Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
92 changes: 78 additions & 14 deletions play-json/jvm/src/main/scala/play/api/libs/json/JsonConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {

Expand Down Expand Up @@ -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".
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Loading