BigNumbersDeserTest.java

package tools.jackson.databind.deser.jdk;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Map;

import org.junit.jupiter.api.Test;

import tools.jackson.core.StreamReadConstraints;
import tools.jackson.core.exc.StreamConstraintsException;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.exc.InvalidFormatException;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;

public class BigNumbersDeserTest
    extends DatabindTestUtil
{
    static class BigIntegerWrapper {
        public BigInteger number;
    }

    static class BigDecimalWrapper {
        public BigDecimal number;
    }

    /*
    /**********************************************************
    /* Test methods
    /**********************************************************
     */

    private final ObjectMapper MAPPER = newJsonMapper();

    private ObjectMapper newJsonMapperWithUnlimitedNumberSizeSupport() {
        JsonFactory jsonFactory = JsonFactory.builder()
                .streamReadConstraints(StreamReadConstraints.builder().maxNumberLength(Integer.MAX_VALUE).build())
                .build();
        return JsonMapper.builder(jsonFactory).build();
    }

    @Test
    public void testDouble() throws Exception
    {
        try {
            MAPPER.readValue(generateJson("d"), DoubleWrapper.class);
            fail("expected StreamReadException");
        } catch (StreamConstraintsException e) {
            verifyException(e, "Number value length", "exceeds the maximum allowed");
        }
    }

    @Test
    public void testDoubleUnlimited() throws Exception
    {
        DoubleWrapper dw =
            newJsonMapperWithUnlimitedNumberSizeSupport().readValue(generateJson("d"), DoubleWrapper.class);
        assertNotNull(dw);
    }

    @Test
    public void testBigDecimal() throws Exception
    {
        try {
            MAPPER.readValue(generateJson("number"), BigDecimalWrapper.class);
            fail("expected StreamReadException");
        } catch (StreamConstraintsException e) {
            verifyException(e, "Number value length ", "exceeds the maximum allowed");
        }
    }

    @Test
    public void testBigDecimalUnlimited() throws Exception
    {
        BigDecimalWrapper bdw =
                newJsonMapperWithUnlimitedNumberSizeSupport()
                        .readValue(generateJson("number"), BigDecimalWrapper.class);
        assertNotNull(bdw);
    }

    @Test
    public void testBigInteger() throws Exception
    {
        try {
            MAPPER.readValue(generateJson("number"), BigIntegerWrapper.class);
            fail("expected StreamReadException");
        } catch (StreamConstraintsException e) {
            verifyException(e, "Number value length", "exceeds the maximum allowed");
        }
    }

    @Test
    public void testBigIntegerUnlimited() throws Exception
    {
        BigIntegerWrapper bdw =
                newJsonMapperWithUnlimitedNumberSizeSupport()
                        .readValue(generateJson("number"), BigIntegerWrapper.class);
        assertNotNull(bdw);
    }

    // [databind#6165]
    // Number length limits must apply to `BigInteger`/`BigDecimal` used as Map keys,
    // not just as values: the key path fed strings straight to the O(n^2)
    // `BigInteger(String)`/`BigDecimal(String)` constructors, bounded only by the far
    // larger max-name-length limit.
    @Test
    public void bigIntegerAsMapKey() throws Exception
    {
        _verifyKeyTooLong(MAPPER, "1".repeat(1200), new TypeReference<Map<BigInteger, String>>() { });
    }

    @Test
    public void bigDecimalAsMapKey() throws Exception
    {
        _verifyKeyTooLong(MAPPER, "1".repeat(1200), new TypeReference<Map<BigDecimal, String>>() { });
    }

    // Same limit applies to `Float`/`Double` keys as well
    @Test
    public void floatingPointAsMapKey() throws Exception
    {
        _verifyKeyTooLong(MAPPER, "1".repeat(1200), new TypeReference<Map<Double, String>>() { });
        _verifyKeyTooLong(MAPPER, "1".repeat(1200), new TypeReference<Map<Float, String>>() { });
    }

    @Test
    public void bigNumberMapKeysWithinLimit() throws Exception
    {
        Map<BigInteger, String> mi = MAPPER.readValue("{\"12345678901234567890\":\"a\"}",
                new TypeReference<Map<BigInteger, String>>() { });
        assertEquals("a", mi.get(new BigInteger("12345678901234567890")));

        Map<BigDecimal, String> md = MAPPER.readValue("{\"3.14159265358979\":\"b\"}",
                new TypeReference<Map<BigDecimal, String>>() { });
        assertEquals("b", md.get(new BigDecimal("3.14159265358979")));
    }

    // Limit is inclusive: key of exactly `maxNumberLength` digits still accepted,
    // one digit more is not
    @Test
    public void mapKeyAtLengthLimit() throws Exception
    {
        final int maxLen = StreamReadConstraints.defaults().getMaxNumberLength();
        final String key = "1".repeat(maxLen);

        Map<BigInteger, String> m = MAPPER.readValue("{\"" + key + "\":\"a\"}",
                new TypeReference<Map<BigInteger, String>>() { });
        assertEquals("a", m.get(new BigInteger(key)));

        _verifyKeyTooLong(MAPPER, "1".repeat(maxLen + 1),
                new TypeReference<Map<BigInteger, String>>() { });
    }

    // Limit is configurable for keys just like it is for values
    @Test
    public void mapKeyWithLoweredLengthLimit() throws Exception
    {
        JsonFactory f = JsonFactory.builder()
                .streamReadConstraints(StreamReadConstraints.builder().maxNumberLength(20).build())
                .build();
        // 25 digits: acceptable with defaults, too long here
        _verifyKeyTooLong(JsonMapper.builder(f).build(), "1".repeat(25),
                new TypeReference<Map<BigInteger, String>>() { });
    }

    // Malformed (but short enough) keys must still fail as regular "weird key" problems
    @Test
    public void malformedBigNumberMapKey() throws Exception
    {
        try {
            MAPPER.readValue("{\"abc\":\"a\"}", new TypeReference<Map<BigInteger, String>>() { });
            fail("expected InvalidFormatException");
        } catch (InvalidFormatException e) {
            verifyException(e, "Cannot deserialize Map key of type `java.math.BigInteger`");
        }
    }

    private void _verifyKeyTooLong(ObjectMapper mapper, String key, TypeReference<?> targetType)
        throws Exception
    {
        try {
            mapper.readValue("{\"" + key + "\":\"x\"}", targetType);
            fail("expected StreamConstraintsException");
        } catch (StreamConstraintsException e) {
            verifyException(e, "Number value length ("+key.length()+") exceeds the maximum allowed");
        }
    }

    // [databind#4435]
    @Test
    public void testNumberStartingWithDot() throws Exception {
        _testNumberWith(".555555555555555555555555555555");
        _testNumberWith("-.555555555555555555555555555555");
        _testNumberWith("+.555555555555555555555555555555");
    }

    // [databind#4577]
    @Test
    public void testNumberEndingWithDot() throws Exception {
        _testNumberWith("55.");
        _testNumberWith("-55.");
        _testNumberWith("+55.");
    }
    
    private void _testNumberWith(String num) throws Exception
    {
        BigDecimal exp = new BigDecimal(num);
        BigDecimalWrapper w = MAPPER.readValue("{\"number\":\"" + num + "\"}", BigDecimalWrapper.class);
        assertEquals(exp, w.number);
    }

    private String generateJson(final String fieldName) {
        final int len = 1200;
        final StringBuilder sb = new StringBuilder();
        sb.append("{\"")
                .append(fieldName)
                .append("\": ");
        for (int i = 0; i < len; i++) {
            sb.append(1);
        }
        sb.append("}");
        return sb.toString();
    }
}