AsyncSplitSurrogateInFieldName1742Test.java

package tools.jackson.core.unittest.async;

import java.nio.charset.StandardCharsets;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

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.unittest.testutil.AsyncReaderWrapper;

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

// 09-Oct-2026, tatu: [core#1742] feed split inside escaped surrogate pair
//   of a property name; all chunk sizes so every split offset gets covered
class AsyncSplitSurrogateInFieldName1742Test extends AsyncTestBase
{
    private final JsonFactory FACTORY = newStreamFactory();
    private final JsonFactory APOS_FACTORY = JsonFactory.builder()
            .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
            .build();

    @ParameterizedTest
    @ValueSource(booleans = { false, true })
    void splitInSurrogatePair(boolean byteBuffer) throws Exception
    {
        _testAllSplits(byteBuffer, FACTORY, "{\"\\uD83D\\uDE00\":1}");
        _testAllSplits(byteBuffer, FACTORY, "{\"ab\\uD83D\\uDE00cd\":1}");
        _testAllSplits(byteBuffer, FACTORY, "{\"x\":{\"\\uD83D\\uDE00\":[true,\"v\"]}}");
        _testAllSplits(byteBuffer, FACTORY, "{\"\\uD83D\\uDE00\\uD834\\uDD1E\":-1.5e3}");
        _testAllSplits(byteBuffer, FACTORY, "{\"a\":1,\"\\ud834\\udd1e\":\"\\ud834\\udd1e\"}");
    }

    @ParameterizedTest
    @ValueSource(booleans = { false, true })
    void splitInSurrogatePairApos(boolean byteBuffer) throws Exception
    {
        _testAllSplits(byteBuffer, APOS_FACTORY, "{'\\uD83D\\uDE00':1}");
        _testAllSplits(byteBuffer, APOS_FACTORY, "{'ab\\uD83D\\uDE00cd':'x'}");
    }

    // End-of-input within surrogate pair escape must fail, not hang or hit internal error
    @ParameterizedTest
    @ValueSource(booleans = { false, true })
    void eofInSurrogatePair(boolean byteBuffer) throws Exception
    {
        for (String doc : new String[] {
                "{\"\\uD83D", "{\"\\uD83D\\", "{\"\\uD83D\\u", "{\"\\uD83D\\uDE", "{\"\\uD83D\\uDE0"
        }) {
            _testAllSplitsFail(byteBuffer, FACTORY, doc,
                    "Unexpected end-of-input in character escape sequence");
        }
    }

    @ParameterizedTest
    @ValueSource(booleans = { false, true })
    void brokenSurrogatePair(boolean byteBuffer) throws Exception
    {
        _testAllSplitsFail(byteBuffer, FACTORY, "{\"\\uD83D\":1}", "Broken surrogate pair");
        _testAllSplitsFail(byteBuffer, FACTORY, "{\"\\uD83Dx\":1}", "Broken surrogate pair");
        _testAllSplitsFail(byteBuffer, FACTORY, "{\"\\uD83D\\u0041\":1}", "Broken surrogate pair");
        _testAllSplitsFail(byteBuffer, FACTORY, "{\"\\uD83D\\n\":1}", "Broken surrogate pair");
        _testAllSplitsFail(byteBuffer, FACTORY, "{\"\\uD83D\\uDEx0\":1}", "expected a hex-digit");
    }

    private void _testAllSplits(boolean byteBuffer, JsonFactory f, String json) throws Exception
    {
        final byte[] doc = json.getBytes(StandardCharsets.UTF_8);
        final String exp = _blockingTokens(f, doc);
        for (int chunk = 1; chunk <= doc.length; ++chunk) {
            try (AsyncReaderWrapper r = _wrap(byteBuffer, f, chunk, doc)) {
                assertEquals(exp, _asyncTokens(r), "chunk size "+chunk+", byteBuffer="+byteBuffer);
            }
        }
    }

    private void _testAllSplitsFail(boolean byteBuffer, JsonFactory f, String json,
            String expMsg) throws Exception
    {
        final byte[] doc = json.getBytes(StandardCharsets.UTF_8);
        for (int chunk = 1; chunk <= doc.length; ++chunk) {
            try (AsyncReaderWrapper r = _wrap(byteBuffer, f, chunk, doc)) {
                String tokens = _asyncTokens(r);
                fail("Should fail for chunk size "+chunk+", byteBuffer="+byteBuffer+"; got: "+tokens);
            } catch (StreamReadException e) {
                verifyException(e, expMsg);
            }
        }
    }

    private AsyncReaderWrapper _wrap(boolean byteBuffer, JsonFactory f, int chunk, byte[] doc)
        throws Exception
    {
        return byteBuffer ? asyncForByteBuffer(f, chunk, doc, 0)
                : asyncForBytes(f, chunk, doc, 0);
    }

    private String _blockingTokens(JsonFactory f, byte[] doc) throws Exception
    {
        StringBuilder sb = new StringBuilder();
        try (JsonParser p = f.createParser(doc)) {
            JsonToken t;
            while ((t = p.nextToken()) != null) {
                sb.append(t).append(':').append(p.getString()).append('|');
            }
        }
        return sb.toString();
    }

    private String _asyncTokens(AsyncReaderWrapper r) throws Exception
    {
        StringBuilder sb = new StringBuilder();
        JsonToken t;
        while ((t = r.nextToken()) != null) {
            sb.append(t).append(':').append(r.currentText()).append('|');
        }
        return sb.toString();
    }
}