AsyncHexNumbers707Test.java

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

import java.math.BigInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.ObjectReadContext;
import tools.jackson.core.StreamReadConstraints;
import tools.jackson.core.async.ByteArrayFeeder;
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.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();

    private final JsonFactory HEX_MAX100_F = JsonFactory.builder()
            .enable(JsonReadFeature.ALLOW_HEXADECIMAL_NUMBERS)
            .streamReadConstraints(StreamReadConstraints.builder()
                    .maxNumberLength(100).build())
            .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");
        }
    }

    @Test
    void hexLengthValidatedWhenInputSplit() throws Exception {
        // Digit count stays below text buffer segment size, so only the
        // check done when input runs out can catch it
        try (JsonParser p = HEX_MAX100_F.createNonBlockingByteArrayParser(ObjectReadContext.empty())) {
            ByteArrayFeeder feeder = (ByteArrayFeeder) p.nonBlockingInputFeeder();
            byte[] doc = ("[0x" + "a".repeat(150)).getBytes("UTF-8");
            feeder.feedInput(doc, 0, doc.length);
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            p.nextToken();
            fail("Should not pass: hex number longer than maxNumberLength");
        } catch (StreamConstraintsException e) {
            verifyException(e, "Number value length (150) exceeds the maximum allowed (100");
        }
    }

    // [core#1765]: signed and unsigned, resumed across multiple feeds, both parser types
    @Test
    void hexLengthValidatedWhenInputSplitMultiFeed() throws Exception {
        for (String sign : new String[] { "", "-" }) {
            byte[] doc = _jsonDoc("[" + sign + "0x" + "a".repeat(150) + "]");
            for (int readSize : new int[] { 1, 5 }) {
                _verifyHexTooLong(asyncForBytes(HEX_MAX100_F, readSize, doc, 0), readSize);
                _verifyHexTooLong(asyncForByteBuffer(HEX_MAX100_F, readSize, doc, 0), readSize);
            }
        }
    }

    // [core#1765]: hex literal of exactly maxNumberLength digits must still pass
    @Test
    void hexAtMaxLengthAcceptedWhenInputSplit() throws Exception {
        for (String sign : new String[] { "", "-" }) {
            String literal = sign + "0x" + "a".repeat(100);
            byte[] doc = _jsonDoc("[" + literal + "]");
            for (int readSize : new int[] { 1, 5, 1000 }) {
                _verifyHexAtMax(asyncForBytes(HEX_MAX100_F, readSize, doc, 0), literal);
                _verifyHexAtMax(asyncForByteBuffer(HEX_MAX100_F, readSize, doc, 0), literal);
            }
        }
    }

    private void _verifyHexTooLong(AsyncReaderWrapper r, int readSize) throws Exception
    {
        try (r) {
            assertToken(JsonToken.START_ARRAY, r.nextToken());
            r.nextToken();
            fail("Should not pass: hex number longer than maxNumberLength");
        } catch (StreamConstraintsException e) {
            verifyException(e, "exceeds the maximum allowed (100");
            // Must fail as soon as accumulated digits exceed the limit, not at end of number
            Matcher m = Pattern.compile("Number value length \\((\\d+)\\)").matcher(e.getMessage());
            assertTrue(m.find(), "Unexpected message: " + e.getMessage());
            int len = Integer.parseInt(m.group(1));
            assertTrue(len > 100 && len <= 100 + readSize,
                    "Unexpected reported length " + len + " (readSize " + readSize + ")");
        }
    }

    private void _verifyHexAtMax(AsyncReaderWrapper r, String literal) throws Exception
    {
        try (r) {
            assertToken(JsonToken.START_ARRAY, r.nextToken());
            assertToken(JsonToken.VALUE_NUMBER_INT, r.nextToken());
            assertEquals(literal, r.currentText());
            assertToken(JsonToken.END_ARRAY, r.nextToken());
        }
    }

    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 + ")");
            }
        }
    }
}