AsyncHexNumbers707Test.java

package tools.jackson.core.unittest.json.async;

import java.math.BigInteger;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JsonToken;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.core.unittest.async.AsyncTestBase;
import tools.jackson.core.unittest.testutil.AsyncReaderWrapper;

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

/**
 * Tests for [core#707]: JSON5-style hexadecimal integer literals via the
 * non-blocking (async) UTF-8 parser. Exercises the {@code 1}-byte and
 * {@code 3}-byte feed sizes to drive suspension at every position.
 */
class AsyncHexNumbers707Test extends AsyncTestBase
{
    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 unsignedHexFullBuffer() throws Exception {
        _expectInt(HEX_F, "0xc0ffee", "0xc0ffee", 0xC0FFEE, 1000);
    }

    @Test
    void unsignedHexOneBytePerRead() throws Exception {
        // Forces suspension after every single byte
        _expectInt(HEX_F, "0xc0ffee", "0xc0ffee", 0xC0FFEE, 1);
    }

    @Test
    void uppercaseHexOneBytePerRead() throws Exception {
        _expectInt(HEX_F, "0XCAFE", "0XCAFE", 0xCAFE, 1);
    }

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

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

    @Test
    void plainZeroStillWorks() throws Exception {
        // Make sure we didn't break the bare "0" path
        try (AsyncReaderWrapper r = asyncForBytes(HEX_F, 1, _jsonDoc(" 0 "), 1)) {
            assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
            assertEquals(0, r.getIntValue());
        }
    }

    @Test
    void hexInsideArray() throws Exception {
        for (int readSize : new int[] {1, 3, 1000}) {
            try (AsyncReaderWrapper r = asyncForBytes(HEX_F, readSize,
                    _jsonDoc("[0x1, 0xFF, -0x10]"), 1)) {
                assertToken(JsonToken.START_ARRAY, r.nextToken());
                assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
                assertEquals(1, r.getIntValue());
                assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
                assertEquals(255, r.getIntValue());
                assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
                assertEquals(-16, r.getIntValue());
                assertToken(JsonToken.END_ARRAY, r.nextToken());
            }
        }
    }

    @Test
    void hexBigIntegerRange() throws Exception {
        final String literal = "0x1ffffffffffffffff";
        final BigInteger expected = new BigInteger("1ffffffffffffffff", 16);
        for (int readSize : new int[] {1, 5, 1000}) {
            try (AsyncReaderWrapper r = asyncForBytes(HEX_F, readSize,
                    _jsonDoc(" " + literal + " "), 1)) {
                assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
                assertEquals(literal, r.currentText());
                assertEquals(expected, r.getBigIntegerValue());
            }
        }
    }

    @Test
    void hexNegativeBigIntegerRange() throws Exception {
        // 17 hex digits with sign -> must promote to (negative) BigInteger via async resumption
        final String literal = "-0x1ffffffffffffffff";
        final BigInteger expected = new BigInteger("-1ffffffffffffffff", 16);
        for (int readSize : new int[] {1, 5, 1000}) {
            try (AsyncReaderWrapper r = asyncForBytes(HEX_F, readSize,
                    _jsonDoc(" " + literal + " "), 1)) {
                assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
                assertEquals(literal, r.currentText());
                assertEquals(expected, r.getBigIntegerValue());
            }
        }
    }

    @Test
    void hex16DigitsLongMax() throws Exception {
        // 16 digits, top nibble == 7 -> stays on the long fast path (Long.MAX_VALUE).
        // Boundary case in _parseHexInt: hexLen == 16 && topNibble < 0x8.
        final String literal = "0x7fffffffffffffff";
        for (int readSize : new int[] {1, 5, 1000}) {
            try (AsyncReaderWrapper r = asyncForBytes(HEX_F, readSize,
                    _jsonDoc(" " + literal + " "), 1)) {
                assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
                assertEquals(literal, r.currentText());
                assertEquals(Long.MAX_VALUE, r.getLongValue());
            }
        }
    }

    @Test
    void hex16DigitsOverflowsToBigInteger() throws Exception {
        // 16 digits, top nibble == 8 -> falls off the long fast path into the
        // BigInteger arm of _parseHexInt (value is 2^63, just past Long.MAX_VALUE).
        final String literal = "0x8000000000000000";
        final BigInteger expected = BigInteger.ONE.shiftLeft(63);
        for (int readSize : new int[] {1, 5, 1000}) {
            try (AsyncReaderWrapper r = asyncForBytes(HEX_F, readSize,
                    _jsonDoc(" " + literal + " "), 1)) {
                assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
                assertEquals(literal, r.currentText());
                assertEquals(expected, r.getBigIntegerValue());
            }
        }
    }

    @Test
    void hexRejectedWhenFeatureDisabled() throws Exception {
        JsonFactory plain = new JsonFactory();
        try (AsyncReaderWrapper r = asyncForBytes(plain, 1, _jsonDoc(" 0xff "), 1)) {
            r.nextToken();
            fail("Should not pass when ALLOW_HEXADECIMAL_NUMBERS is disabled");
        } catch (StreamReadException e) {
            // Error now names the feature that must be enabled (see _checkHexNumbersAllowed).
            verifyException(e, "Unexpected character ('x'");
            verifyException(e, "ALLOW_HEXADECIMAL_NUMBERS");
        }
    }

    @Test
    void hexPrefixWithoutDigitsFails() throws Exception {
        try (AsyncReaderWrapper r = asyncForBytes(HEX_F, 1, _jsonDoc(" 0x "), 1)) {
            r.nextToken();
            fail("Should not pass: prefix without any hex digit");
        } catch (StreamReadException e) {
            verifyException(e, "hex digit");
        }
    }

    private void _expectInt(JsonFactory factory, String literal, String expectedText,
            long expectedValue, int readSize) throws Exception
    {
        String input = " " + literal + " ";
        try (AsyncReaderWrapper r = asyncForBytes(factory, readSize, _jsonDoc(input), 1)) {
            assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
            assertEquals(expectedText, r.currentText(),
                    "currentText() literal mismatch for " + literal + " (readSize " + readSize + ")");
            assertEquals(expectedValue, r.getLongValue(),
                    "long value mismatch for " + literal + " (readSize " + readSize + ")");
            if (expectedValue >= Integer.MIN_VALUE && expectedValue <= Integer.MAX_VALUE) {
                assertEquals((int) expectedValue, r.getIntValue(),
                        "int value mismatch for " + literal + " (readSize " + readSize + ")");
            }
        }
    }
}