NonStandardHexNumbers707Test.java

package tools.jackson.core.unittest.read;

import java.math.BigInteger;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.StreamReadConstraints;
import tools.jackson.core.exc.StreamConstraintsException;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.core.unittest.JacksonCoreTestBase;

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

/**
 * Tests for [core#707]: JSON5-style hexadecimal integer literals enabled via
 * {@link JsonReadFeature#ALLOW_HEXADECIMAL_NUMBERS}.
 */
class NonStandardHexNumbers707Test extends JacksonCoreTestBase
{
    private final JsonFactory HEX_F = JsonFactory.builder()
            .enable(JsonReadFeature.ALLOW_HEXADECIMAL_NUMBERS)
            .build();

    private final JsonFactory HEX_AND_PLUS_F = JsonFactory.builder()
            .enable(JsonReadFeature.ALLOW_HEXADECIMAL_NUMBERS)
            .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS)
            .build();

    @Test
    void unsignedHexLowercase() throws Exception {
        _expectInt(HEX_F, "0xc0ffee", "0xc0ffee", 0xC0FFEE);
    }

    @Test
    void unsignedHexUppercaseXAndDigits() throws Exception {
        _expectInt(HEX_F, "0XC0FFEE", "0XC0FFEE", 0xC0FFEE);
    }

    @Test
    void unsignedHexZero() throws Exception {
        _expectInt(HEX_F, "0x0", "0x0", 0);
    }

    @Test
    void unsignedHexWithLeadingZeros() throws Exception {
        // [core#707]: leading zeros are always permitted in hex digits, regardless
        // of ALLOW_LEADING_ZEROS_FOR_NUMBERS.
        _expectInt(HEX_F, "0x007F", "0x007F", 0x7F);
    }

    @Test
    void negativeHex() throws Exception {
        _expectInt(HEX_F, "-0x10", "-0x10", -16);
    }

    @Test
    void plusSignHexRequiresLeadingPlusFeature() throws Exception {
        // Without ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS, +0xff must still fail
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(HEX_F, mode, " +0xff ")) {
                p.nextToken();
                fail("Should not pass when ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS is disabled");
            } catch (StreamReadException e) {
                verifyException(e, "plus sign");
            }
        }
    }

    @Test
    void plusSignHexWithPlusFeature() throws Exception {
        _expectInt(HEX_AND_PLUS_F, "+0xff", "+0xff", 0xFF);
    }

    @Test
    void hexLongRange() throws Exception {
        // Just over Integer.MAX_VALUE (0x7fffffff = 2147483647) -> 0x80000000 = 2147483648L
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(HEX_F, mode, " 0x80000000 ")) {
                assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
                assertEquals("0x80000000", p.getString());
                assertEquals(2147483648L, p.getLongValue());
            }
        }
    }

    @Test
    void hexBigIntegerRange() throws Exception {
        // 17 hex digits -> must promote to BigInteger
        final String literal = "0x1ffffffffffffffff";
        final BigInteger expected = new BigInteger("1ffffffffffffffff", 16);
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(HEX_F, mode, " " + literal + " ")) {
                assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
                assertEquals(literal, p.getString());
                assertEquals(expected, p.getBigIntegerValue());
            }
        }
    }

    @Test
    void hexInsideArray() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(HEX_F, mode, "[0x1, 0x2, -0xA]")) {
                assertToken(JsonToken.START_ARRAY, p.nextToken());
                assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
                assertEquals(1, p.getIntValue());
                assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
                assertEquals(2, p.getIntValue());
                assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
                assertEquals(-10, p.getIntValue());
                assertToken(JsonToken.END_ARRAY, p.nextToken());
            }
        }
    }

    @Test
    void hexRejectedWhenFeatureDisabled() throws Exception {
        // With the feature OFF, 0x... must still fail like in vanilla JSON,
        // and the error message must point at the feature to enable.
        JsonFactory plainF = new JsonFactory();
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(plainF, mode, " 0xc0ffee ")) {
                p.nextToken();
                fail("Should not pass when ALLOW_HEXADECIMAL_NUMBERS is disabled");
            } catch (StreamReadException e) {
                verifyException(e, "Unexpected character ('x'");
                verifyException(e, "ALLOW_HEXADECIMAL_NUMBERS");
            }
        }
    }

    @Test
    void hexPrefixWithoutDigitsFails() throws Exception {
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(HEX_F, mode, " 0x ")) {
                p.nextToken();
                fail("Should not pass: prefix without any hex digit");
            } catch (StreamReadException e) {
                verifyException(e, "hex digit");
            }
        }
    }

    @Test
    void hexNumberLengthConstraint() throws Exception {
        // Build a hex literal long enough to cross several TextBuffer segment
        // boundaries (default first segment is 500 chars; using 8000 digits to
        // ensure we definitely span at least one boundary on every backend).
        StringBuilder sb = new StringBuilder("0x");
        for (int i = 0; i < 8000; i++) {
            sb.append('f');
        }
        final String hugeHex = sb.toString();
        JsonFactory cappedF = JsonFactory.builder()
                .enable(JsonReadFeature.ALLOW_HEXADECIMAL_NUMBERS)
                .streamReadConstraints(StreamReadConstraints.builder().maxNumberLength(100).build())
                .build();
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(cappedF, mode, " " + hugeHex + " ")) {
                p.nextToken();
                fail("Should not pass: hex literal exceeds maxNumberLength (mode " + mode + ")");
            } catch (StreamConstraintsException e) {
                verifyException(e, "exceeds the maximum");
            }
        }
    }

    private void _expectInt(JsonFactory factory, String literal, String expectedText, long expectedValue)
            throws Exception
    {
        String input = " " + literal + " ";
        for (int mode : ALL_MODES) {
            try (JsonParser p = createParser(factory, mode, input)) {
                assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
                assertEquals(expectedText, p.getString(),
                        "getString() literal mismatch for " + literal + " (mode " + mode + ")");
                assertEquals(expectedValue, p.getLongValue(),
                        "long value mismatch for " + literal + " (mode " + mode + ")");
                if (expectedValue >= Integer.MIN_VALUE && expectedValue <= Integer.MAX_VALUE) {
                    assertEquals((int) expectedValue, p.getIntValue(),
                            "int value mismatch for " + literal + " (mode " + mode + ")");
                }
            }
        }
    }
}