AsyncEOFMessagesTest.java

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

import org.junit.jupiter.api.Test;

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

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

// 09-Oct-2026, tatu: end-of-input within token should give specific error message
//   (not "internal state"); and BOM-only content is same as empty content
class AsyncEOFMessagesTest extends AsyncTestBase
{
    private final JsonFactory F = JsonFactory.builder()
            .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
            .enable(JsonReadFeature.ALLOW_UNQUOTED_PROPERTY_NAMES)
            .enable(JsonReadFeature.ALLOW_JAVA_COMMENTS)
            .enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS)
            .build();

    private final static byte[] BOM = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };

    @Test
    void eofInFieldName() throws Exception
    {
        _testEOF(utf8Bytes("{\"ab"), "Unexpected end-of-input in property name");
        _testEOF(utf8Bytes("{'ab"), "Unexpected end-of-input in property name");
        _testEOF(utf8Bytes("{ab"), "Unexpected end-of-input in property name");
    }

    @Test
    void eofBetweenTokens() throws Exception
    {
        for (String doc : new String[] {
                "{ ", "{\"a\":1,", "{\"a\":1, ", "{\"a\"", "{\"a\" ", "{\"a\":1 "
        }) {
            _testEOF(utf8Bytes(doc), "expected close marker for Object");
        }
        for (String doc : new String[] { "[ ", "[1 ", "[1,", "[1, " }) {
            _testEOF(utf8Bytes(doc), "expected close marker for Array");
        }
    }

    @Test
    void eofInNumberSign() throws Exception
    {
        _testEOF(utf8Bytes("+"), "Unexpected end-of-input in a Number value");
        _testEOF(utf8Bytes("[-"), "Unexpected end-of-input in a Number value");
    }

    // Number with no digits after decimal point or exponent sign is invalid
    @Test
    void eofInNumberWithoutDigits() throws Exception
    {
        _testEOF(utf8Bytes("1."), "Decimal point not followed by a digit");
        _testEOF(utf8Bytes("-1."), "Decimal point not followed by a digit");
        _testEOF(utf8Bytes("[1."), "Decimal point not followed by a digit");
        _testEOF(utf8Bytes("1e+"), "was expecting digits after exponent marker");
        _testEOF(utf8Bytes("1.5e-"), "was expecting digits after exponent marker");
    }

    @Test
    void eofAfterTrailingDecimalPointAllowed() throws Exception
    {
        final JsonFactory f = JsonFactory.builder()
                .enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS)
                .build();
        final byte[] doc = utf8Bytes("1.");
        for (int readSize = 1; readSize <= doc.length; ++readSize) {
            try (AsyncReaderWrapper p = asyncForBytes(f, readSize, doc, 0)) {
                assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
                assertEquals(1.0, p.getDoubleValue());
                assertNull(p.nextToken());
            }
        }
    }

    @Test
    void eofInCommentStart() throws Exception
    {
        _testEOF(utf8Bytes("/"), "Unexpected end-of-input in a comment");
        _testEOF(utf8Bytes("[/"), "Unexpected end-of-input in a comment");
    }

    @Test
    void eofInEscape() throws Exception
    {
        _testEOF(utf8Bytes("{\"a\\u00"), "Unexpected end-of-input in character escape sequence");
        _testEOF(utf8Bytes("\"a\\u00"), "Unexpected end-of-input in character escape sequence");
        _testEOF(utf8Bytes("[\"a\\"), "Unexpected end-of-input in character escape sequence");
    }

    @Test
    void eofInStringValue() throws Exception
    {
        _testEOF(utf8Bytes("\"ab"), "Unexpected end-of-input in a String value");
        _testEOF(utf8Bytes("['ab"), "Unexpected end-of-input in a String value");
        // and with partial multi-byte UTF-8 characters
        _testEOF(new byte[] { '"', 'a', (byte) 0xC3 }, "Unexpected end-of-input in a String value");
        _testEOF(new byte[] { '"', 'a', (byte) 0xE2, (byte) 0x82 },
                "Unexpected end-of-input in a String value");
        _testEOF(new byte[] { '"', 'a', (byte) 0xF0, (byte) 0x9F, (byte) 0x98 },
                "Unexpected end-of-input in a String value");
    }

    @Test
    void eofInBOM() throws Exception
    {
        _testEOF(new byte[] { (byte) 0xEF }, "Unexpected end-of-input in UTF-8 BOM");
        _testEOF(new byte[] { (byte) 0xEF, (byte) 0xBB }, "Unexpected end-of-input in UTF-8 BOM");
    }

    @Test
    void bomOnlyContent() throws Exception
    {
        for (int readSize = 1; readSize <= BOM.length; ++readSize) {
            for (boolean byteBuffer : new boolean[] { false, true }) {
                try (AsyncReaderWrapper p = _async(byteBuffer, readSize, BOM)) {
                    assertNull(p.nextToken());
                }
            }
        }
        // and with all content (and end-of-input) available right away
        try (NonBlockingByteArrayJsonParser p = (NonBlockingByteArrayJsonParser)
                F.createNonBlockingByteArrayParser(ObjectReadContext.empty())) {
            p.feedInput(BOM, 0, BOM.length);
            p.endOfInput();
            assertNull(p.nextToken());
        }
    }

    private void _testEOF(byte[] doc, String expMsg) throws Exception
    {
        for (int readSize = 1; readSize <= doc.length; ++readSize) {
            for (boolean byteBuffer : new boolean[] { false, true }) {
                try (AsyncReaderWrapper p = _async(byteBuffer, readSize, doc)) {
                    while (p.nextToken() != null) { }
                    fail("Should not pass for readSize "+readSize+", byteBuffer="+byteBuffer);
                } catch (StreamReadException e) {
                    verifyException(e, expMsg);
                }
            }
        }
    }

    private AsyncReaderWrapper _async(boolean byteBuffer, int readSize, byte[] doc)
        throws Exception
    {
        return byteBuffer ? asyncForByteBuffer(F, readSize, doc, 0)
                : asyncForBytes(F, readSize, doc, 0);
    }
}