DecimalNodeBigIntegerScaleTest.java
package tools.jackson.databind.node;
import java.math.BigDecimal;
import java.math.BigInteger;
import org.junit.jupiter.api.Test;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.exc.StreamConstraintsException;
import tools.jackson.databind.*;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests to verify that conversion of {@link DecimalNode} to {@link BigInteger}
* is guarded against excessive {@link BigDecimal} scale magnitude (which would
* make conversion extremely expensive), same as streaming parsers are.
*
* @see <a href="https://github.com/FasterXML/jackson-databind/issues/6214">[databind#6214]</a>
*/
public class DecimalNodeBigIntegerScaleTest extends DatabindTestUtil
{
private final ObjectMapper MAPPER = jsonMapperBuilder()
.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
.build();
// Short text, but scale magnitude way beyond limit (100k)
private final static String HUGE_NEG_SCALE = "1e2000000000"; // scale -2_000_000_000
private final static String HUGE_POS_SCALE = "1e-2000000000"; // scale +2_000_000_000
@Test
public void testAccessorsGuarded() throws Exception
{
// Huge negative scale (integral value): strict accessors must fail...
DecimalNode neg = DecimalNode.valueOf(new BigDecimal(HUGE_NEG_SCALE));
_verifyGuarded(HUGE_NEG_SCALE, () -> neg.bigIntegerValue());
_verifyGuarded(HUGE_NEG_SCALE, () -> neg.asBigInteger());
// ... but default/Optional variants must return default/empty instead
// of throwing, same as for other conversion failures (NaN, fraction)
assertEquals(BigInteger.ONE, neg.bigIntegerValue(BigInteger.ONE));
assertFalse(neg.bigIntegerValueOpt().isPresent());
assertEquals(BigInteger.ONE, neg.asBigInteger(BigInteger.ONE));
assertFalse(neg.asBigIntegerOpt().isPresent());
// Huge positive scale (has fractional part): strict `bigIntegerValue()`
// variants reject it earlier (fraction check, cheap); coercing
// `asBigInteger()` variants would truncate and so must be guarded
DecimalNode pos = DecimalNode.valueOf(new BigDecimal(HUGE_POS_SCALE));
assertTrue(pos.hasFractionalPart());
_verifyGuarded(HUGE_POS_SCALE, () -> pos.asBigInteger());
assertEquals(BigInteger.ONE, pos.asBigInteger(BigInteger.ONE));
assertFalse(pos.asBigIntegerOpt().isPresent());
}
// POJONode wrapping a BigDecimal has its own conversion path
@Test
public void testPOJONodeAccessorsGuarded() throws Exception
{
for (String num : new String[] { HUGE_NEG_SCALE, HUGE_POS_SCALE }) {
POJONode n = new POJONode(new BigDecimal(num));
// Strict accessor must fail...
_verifyGuarded(num, () -> n.asBigInteger());
// ... but default/Optional variants return default/empty, as with
// `DecimalNode` (and other conversion failures)
assertEquals(BigInteger.ONE, n.asBigInteger(BigInteger.ONE));
assertFalse(n.asBigIntegerOpt().isPresent());
}
assertEquals(new BigInteger("1000"), new POJONode(new BigDecimal("1e3")).asBigInteger());
}
@Test
public void testViaTreeTraversingParser() throws Exception
{
JsonNode root = MAPPER.readTree("{\"v\": " + HUGE_NEG_SCALE + "}");
assertTrue(root.get("v").isBigDecimal());
try (JsonParser p = MAPPER.treeAsTokens(root)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
_verifyGuarded(HUGE_NEG_SCALE, () -> p.getBigIntegerValue());
}
}
@Test
public void testViaConversion() throws Exception
{
JsonNode n = MAPPER.readTree(HUGE_NEG_SCALE);
_verifyGuarded(HUGE_NEG_SCALE, () -> MAPPER.treeToValue(n, BigInteger.class));
}
// Values within limits must still convert
@Test
public void testWithinLimits() throws Exception
{
assertEquals(new BigInteger("1000"), DecimalNode.valueOf(new BigDecimal("1e3")).bigIntegerValue());
assertEquals(BigInteger.ZERO, DecimalNode.valueOf(new BigDecimal("1e-3")).asBigInteger());
// scale magnitude exactly at limit (100_000) is fine
BigDecimal atLimit = new BigDecimal("1e-100000");
assertEquals(BigInteger.ZERO, DecimalNode.valueOf(atLimit).asBigInteger());
assertEquals(BigInteger.ZERO, DecimalNode.valueOf(atLimit).asBigInteger(BigInteger.TEN));
// ... and so is the other (expensive but legal) direction: negative scale
// at limit yields an actual 100k-digit integer, and must NOT be rejected
DecimalNode atNegLimit = DecimalNode.valueOf(new BigDecimal("1e100000"));
assertEquals(-100_000, atNegLimit.decimalValue().scale());
assertEquals(332193, atNegLimit.bigIntegerValue().bitLength());
assertEquals(332193, atNegLimit.asBigInteger(BigInteger.ONE).bitLength());
assertTrue(atNegLimit.bigIntegerValueOpt().isPresent());
}
private interface Conversion { Object convert() throws Exception; }
private void _verifyGuarded(String num, Conversion c) throws Exception
{
try {
c.convert();
fail("Should not pass for: " + num);
} catch (StreamConstraintsException e) {
verifyException(e, "BigDecimal scale");
verifyException(e, "magnitude exceeds the maximum allowed");
}
}
}