NonStandardNumberParsingTest.java

package tools.jackson.core.unittest.read;

import java.math.BigDecimal;
import java.util.List;

import org.junit.jupiter.api.MethodOrderer.MethodName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.io.SerializedString;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.core.sym.PropertyNameMatcher;
import tools.jackson.core.unittest.*;
import tools.jackson.core.util.Named;

import static org.junit.jupiter.api.Assertions.*;

// easier to read on IDE
@TestMethodOrder(MethodName.class)
class NonStandardNumberParsingTest
    extends JacksonCoreTestBase
{
    private final JsonFactory JSON_F = JsonFactory.builder()
            .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS)
            .enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS)
            .enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS)
            .build();

    protected JsonFactory jsonFactory() {
        return JSON_F;
    }

    /**
     * The format "0xc0ffee" is not valid JSON, so this should fail
     */
    @Test
    void hexadecimal() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, " 0xc0ffee ")) {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('x'");
            }
        }
    }

    @Test
    void hexadecimalBigX() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, " 0XC0FFEE ")) {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('x'");
            }
        }
    }

    @Test
    void negativeHexadecimal() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, " -0xc0ffee ")) {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('x'");
            }
        }
    }

    /**
     * Test for checking that Overflow for Double value does not lead
     * to bogus NaN information.
     */
    @Test
    void largeDecimal() throws Exception {
        final String biggerThanDouble = "7976931348623157e309";
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, " " + biggerThanDouble + " ")) {
                assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
                assertEquals(new BigDecimal(biggerThanDouble), p.getDecimalValue());
                assertFalse(p.isNaN());
                assertEquals(Double.POSITIVE_INFINITY, p.getValueAsDouble());
                // 01-Dec-2023, tatu: [core#1137] NaN only from explicit value
                assertFalse(p.isNaN());
            }
        }
    }

    // JSON does not allow numbers to have f or d suffixes
    @Test
    void floatMarker() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, " -0.123f ")) {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('f'");
            }
        }
    }

    //JSON does not allow numbers to have f or d suffixes
    @Test
    void doubleMarker() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, " -0.123d ")) {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('d'");
            }
        }
    }

    @Test
    void test2DecimalPoints() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, " -0.123.456 ")) {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('.'");
            }
        }
    }

    // [core#679]: second decimal point must be caught inside containers too
    @Test
    void test2DecimalPointsInArray() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, "[ 1.5.00 ]")) {
                assertEquals(JsonToken.START_ARRAY, p.nextToken());
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('.'");
                verifyException(e, "more than one decimal point");
            }
        }
    }

    /**
     * The format ".NNN" (as opposed to "0.NNN") is not valid JSON, so this should fail
     */
    @Test
    void leadingDotInDecimal() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, " .123 ")) {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('.'");
            }
        }
    }

    /*
     * The format "+NNN" (as opposed to "NNN") is not valid JSON, so this should fail
     */
    @Test
    void leadingPlusSignInDecimalDefaultFail() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(mode, " +123 ")) {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('+' (code 43)) in numeric value: JSON spec does not allow numbers to have plus signs: enable `JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS` to allow");
            }
            try (JsonParser p = createParser(mode, " +0.123 ")) {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('+' (code 43)) in numeric value: JSON spec does not allow numbers to have plus signs: enable `JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS` to allow");
            }
        }
    }

    /**
     * The format "NNN." (as opposed to "NNN") is not valid JSON, so this should fail
     */
    @Test
    void trailingDotInDecimal() throws Exception {
        for (int mode : ALL_MODES) {
            JsonParser p = createParser(mode, " 123. ");
            try {
                p.nextToken();
                fail("Should not pass");
            } catch (StreamReadException e) {
                verifyException(e, "Decimal point not followed by a digit");
            }
            p.close();
        }
    }

    @Test
    public void testLeadingDotInDecimalAllowedDataInput() {
        _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_DATA_INPUT);
    }

    @Test
    public void testLeadingDotInDecimalAllowedBytes() {
        _testLeadingDotInDecimalAllowed(JSON_F, MODE_INPUT_STREAM);
        _testLeadingDotInDecimalAllowed(JSON_F, MODE_INPUT_STREAM_THROTTLED);
    }

    @Test
    public void testLeadingDotInDecimalAllowedReader() {
        _testLeadingDotInDecimalAllowed(JSON_F, MODE_READER);
        _testLeadingDotInDecimalAllowed(jsonFactory(), MODE_READER_THROTTLED);
    }

    @Test
    public void testTrailingDotInDecimalAllowedDataInput() {
        _testTrailingDotInDecimalAllowed(jsonFactory(), MODE_DATA_INPUT);
    }

    @Test
    public void testTrailingDotInDecimalAllowedBytes() {
        _testTrailingDotInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM);
        _testTrailingDotInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM_THROTTLED);
    }

    @Test
    void trailingDotInDecimalAllowedReader() {
        _testTrailingDotInDecimalAllowed(jsonFactory(), MODE_READER);
        _testTrailingDotInDecimalAllowed(jsonFactory(), MODE_READER_THROTTLED);
    }

    @Test
    void leadingPlusSignInDecimalAllowedDataInput() throws Exception {
        _testLeadingPlusSignInDecimalAllowed(jsonFactory(), MODE_DATA_INPUT);
    }

    @Test
    void leadingPlusSignInDecimalAllowedBytes() throws Exception {
        _testLeadingPlusSignInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM);
        _testLeadingPlusSignInDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM_THROTTLED);
    }

    @Test
    void leadingPlusSignInDecimalAllowedReader() throws Exception {
        _testLeadingPlusSignInDecimalAllowed(jsonFactory(), MODE_READER);
        _testLeadingPlusSignInDecimalAllowed(jsonFactory(), MODE_READER_THROTTLED);
    }

    // [core#1630]: nextName() value-peeking must also honor
    // ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS (as nextToken() does)
    @Test
    void leadingDotAsFieldValueBytes() throws Exception {
        _testLeadingDotAsFieldValue(jsonFactory(), MODE_INPUT_STREAM);
        _testLeadingDotAsFieldValue(jsonFactory(), MODE_INPUT_STREAM_THROTTLED);
    }

    @Test
    void leadingDotAsFieldValueReader() throws Exception {
        _testLeadingDotAsFieldValue(jsonFactory(), MODE_READER);
        _testLeadingDotAsFieldValue(jsonFactory(), MODE_READER_THROTTLED);
    }

    @Test
    void leadingDotAsFieldValueDataInput() throws Exception {
        _testLeadingDotAsFieldValue(jsonFactory(), MODE_DATA_INPUT);
    }

    @Test
    void signedLeadingDotAsFieldValueBytes() throws Exception {
        _testSignedLeadingDotAsFieldValue(jsonFactory(), MODE_INPUT_STREAM);
        _testSignedLeadingDotAsFieldValue(jsonFactory(), MODE_INPUT_STREAM_THROTTLED);
    }

    @Test
    void signedLeadingDotAsFieldValueReader() throws Exception {
        _testSignedLeadingDotAsFieldValue(jsonFactory(), MODE_READER);
        _testSignedLeadingDotAsFieldValue(jsonFactory(), MODE_READER_THROTTLED);
    }

    @Test
    void signedLeadingDotAsFieldValueDataInput() throws Exception {
        _testSignedLeadingDotAsFieldValue(jsonFactory(), MODE_DATA_INPUT);
    }

    @Test
    void leadingDotAsFieldValueFailsWhenDisabledBytes() throws Exception {
        _testLeadingDotAsFieldValueFails(newStreamFactory(), MODE_INPUT_STREAM);
        _testLeadingDotAsFieldValueFails(newStreamFactory(), MODE_INPUT_STREAM_THROTTLED);
    }

    @Test
    void leadingDotAsFieldValueFailsWhenDisabledReader() throws Exception {
        _testLeadingDotAsFieldValueFails(newStreamFactory(), MODE_READER);
        _testLeadingDotAsFieldValueFails(newStreamFactory(), MODE_READER_THROTTLED);
    }

    @Test
    void leadingDotAsFieldValueFailsWhenDisabledDataInput() throws Exception {
        _testLeadingDotAsFieldValueFails(newStreamFactory(), MODE_DATA_INPUT);
    }

    @Test
    void leadingDotInNegativeDecimalAllowedAsync() throws Exception {
        _testLeadingDotInNegativeDecimalAllowed(jsonFactory(), MODE_DATA_INPUT);
    }

    @Test
    void leadingDotInNegativeDecimalAllowedBytes() throws Exception {
        _testLeadingDotInNegativeDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM);
        _testLeadingDotInNegativeDecimalAllowed(jsonFactory(), MODE_INPUT_STREAM_THROTTLED);
    }

    @Test
    void leadingDotInNegativeDecimalAllowedReader() throws Exception {
        _testLeadingDotInNegativeDecimalAllowed(jsonFactory(), MODE_READER);
        _testLeadingDotInNegativeDecimalAllowed(jsonFactory(), MODE_READER_THROTTLED);
    }

    private void _testLeadingDotInDecimalAllowed(JsonFactory f, int mode)
    {
        try (JsonParser p = createParser(f, mode, " .125 ")) {
            assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(0.125, p.getValueAsDouble());
            assertEquals("0.125", p.getDecimalValue().toString());
            assertEquals(".125", p.getString());
        }
    }

    private void _testLeadingPlusSignInDecimalAllowed(JsonFactory f, int mode)
    {
        try (JsonParser p = createParser(f, mode, " +125 ")) {
            assertEquals(JsonToken.VALUE_NUMBER_INT, p.nextToken());
            assertEquals(125.0, p.getValueAsDouble());
            assertEquals("125", p.getDecimalValue().toString());
            // [core#784]: Leading plus sign should be included in textual representation
            assertEquals("+125", p.getString());
        }
        try (JsonParser p = createParser(f, mode, " +0.125 ")) {
            assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(0.125, p.getValueAsDouble());
            assertEquals("0.125", p.getDecimalValue().toString());
            // [core#784]: Leading plus sign should be included in textual representation
            assertEquals("+0.125", p.getString());
        }
        try (JsonParser p = createParser(f, mode, " +.125 ")) {
            assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(0.125, p.getValueAsDouble());
            assertEquals("0.125", p.getDecimalValue().toString());
            // [core#784]: Leading plus sign should be included in textual representation
            assertEquals("+.125", p.getString());
        }
    }

    private void _testTrailingDotInDecimalAllowed(JsonFactory f, int mode)
    {
        try (JsonParser p = createParser(f, mode, " 125. ")) {
            assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(125.0, p.getValueAsDouble());
            assertEquals("125", p.getDecimalValue().toString());
            assertEquals("125.", p.getString());
        }
    }

    // [core#1630]: leading decimal point in value peeked by nextName()
    private void _testLeadingDotAsFieldValue(JsonFactory f, int mode)
    {
        final String DOC = "{\"a\": .125}";

        // 1) nextName() (no-arg) peeks the value after resolving name
        try (JsonParser p = createParser(f, mode, DOC)) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
            assertEquals("a", p.nextName());
            assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(0.125, p.getValueAsDouble());
            assertEquals(".125", p.getString());
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }

        // 2) nextName(SerializableString), matching name (the "Yes" fast path)
        try (JsonParser p = createParser(f, mode, DOC)) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
            assertTrue(p.nextName(new SerializedString("a")));
            assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(0.125, p.getValueAsDouble());
            assertEquals(".125", p.getString());
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }

        // 3) nextName(SerializableString), non-matching name (the "Maybe" path)
        try (JsonParser p = createParser(f, mode, DOC)) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
            assertFalse(p.nextName(new SerializedString("b")));
            assertEquals("a", p.currentName());
            assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(0.125, p.getValueAsDouble());
            assertEquals(".125", p.getString());
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }

        // 4) nextNameMatch(), which peeks the value too
        final PropertyNameMatcher matcher = f.constructNameMatcher(List.of(Named.fromString("a")), false);
        try (JsonParser p = createParser(f, mode, DOC)) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
            assertEquals(0, p.nextNameMatch(matcher));
            assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(0.125, p.getValueAsDouble());
            assertEquals(".125", p.getString());
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }
    }

    // [core#1630]: signed variants of leading decimal point, as peeked property value
    private void _testSignedLeadingDotAsFieldValue(JsonFactory f, int mode)
    {
        try (JsonParser p = createParser(f, mode, "{\"a\": -.125}")) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
            assertEquals("a", p.nextName());
            assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(-0.125, p.getValueAsDouble());
            assertEquals("-.125", p.getString());
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }
        try (JsonParser p = createParser(f, mode, "{\"a\": +.125}")) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
            assertEquals("a", p.nextName());
            assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(0.125, p.getValueAsDouble());
            assertEquals("+.125", p.getString());
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }
    }

    // [core#1630]: with feature disabled, nextName() must still report the same
    // failure that nextToken() does -- the peek path must not silently allow it
    private void _testLeadingDotAsFieldValueFails(JsonFactory f, int mode)
    {
        try (JsonParser p = createParser(f, mode, "{\"a\": .125}")) {
            p.nextToken();
            p.nextName();
            fail("Should not pass");
        } catch (StreamReadException e) {
            verifyException(e, "Unexpected character ('.'");
        }
    }

    private void _testLeadingDotInNegativeDecimalAllowed(JsonFactory f, int mode)
    {
        try (JsonParser p = createParser(f, mode, " -.125 ")) {
            assertEquals(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
            assertEquals(-0.125, p.getValueAsDouble());
            assertEquals("-0.125", p.getDecimalValue().toString());
            assertEquals("-.125", p.getString());
        }
    }
}