ReadStringStreamingTest.java

package tools.jackson.core.unittest.read;

import java.io.StringWriter;
import java.io.Writer;

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.json.JsonFactory;
import tools.jackson.core.unittest.JacksonCoreTestBase;

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

/**
 * Tests for {@link JsonParser#readString(Writer)} streaming functionality
 * (added for [core#1288]).
 *
 * @since 3.1
 */
class ReadStringStreamingTest extends JacksonCoreTestBase
{
    private static final JsonFactory JSON_FACTORY = new JsonFactory();

    // Size of the intermediate output buffer used by _streamString
    private static final int OUT_BUF_SIZE = 1024;

    /*
    /**********************************************************************
    /* Empty and trivial strings
    /**********************************************************************
     */

    @Test
    void emptyString() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testEmptyString(mode);
        }
    }

    private void _testEmptyString(int mode) throws Exception
    {
        JsonParser p = createParser(JSON_FACTORY, mode, "[\"\"]");
        assertToken(JsonToken.START_ARRAY, p.nextToken());
        assertToken(JsonToken.VALUE_STRING, p.nextToken());

        Writer w = new StringWriter();
        long len = p.readString(w);
        assertEquals(0L, len);
        assertEquals("", w.toString());

        assertToken(JsonToken.END_ARRAY, p.nextToken());
        p.close();
    }

    @Test
    void singleCharString() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testSingleCharString(mode);
        }
    }

    private void _testSingleCharString(int mode) throws Exception
    {
        try (JsonParser p = createParser(JSON_FACTORY, mode, "[\"x\"]")) {
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
    
            Writer w = new StringWriter();
            assertEquals(1L, p.readString(w));
            assertEquals("x", w.toString());
        }
    }

    /*
    /**********************************************************************
    /* Escape sequences
    /**********************************************************************
     */

    @Test
    void commonEscapeSequences() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testCommonEscapeSequences(mode);
        }
    }

    private void _testCommonEscapeSequences(int mode) throws Exception
    {
        // \", \\, \/, \b, \f, \n, \r, \t
        String json = "[\"\\\"  \\\\  \\/  \\b  \\f  \\n  \\r  \\t\"]";
        String expected = "\"  \\  /  \b  \f  \n  \r  \t";

        try (JsonParser p = createParser(JSON_FACTORY, mode, json)) {
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
        
            Writer w = new StringWriter();
            long len = p.readString(w);
            assertEquals(expected, w.toString());
            assertEquals((long) expected.length(), len);
        }
    }

    @Test
    void unicodeEscapeSequences() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testUnicodeEscapeSequences(mode);
        }
    }

    private void _testUnicodeEscapeSequences(int mode) throws Exception
    {
        // \u00e9 = ��, \u4e2d = ���, \u0000 = NUL
        String json = "[\"caf\\u00e9 \\u4e2d \\u0000\"]";
        String expected = "caf\u00e9 \u4e2d \u0000";

        try (JsonParser p = createParser(JSON_FACTORY, mode, json)) {
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
    
            Writer w = new StringWriter();
            long len = p.readString(w);
            assertEquals(expected, w.toString());
            assertEquals((long) expected.length(), len);
        }
    }

    @Test
    void escapesAtOutputBufferBoundary() throws Exception
    {
        // Put an escape sequence right at the 512-char flush boundary to
        // ensure the flush-and-continue logic handles partially-seen escapes.
        for (int mode : ALL_MODES) {
            _testEscapesAtOutputBufferBoundary(mode);
        }
    }

    private void _testEscapesAtOutputBufferBoundary(int mode) throws Exception
    {
        // Fill to one char before the buffer flush boundary, then place a \n escape
        String prefix = "x".repeat(OUT_BUF_SIZE - 1);
        String json = "[\"" + prefix + "\\n\"]";
        String expected = prefix + "\n";

        try (JsonParser p = createParser(JSON_FACTORY, mode, json)) {
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
    
            Writer w = new StringWriter();
            long len = p.readString(w);
            assertEquals(expected, w.toString());
            assertEquals((long) expected.length(), len);
        }
    }

    /*
    /**********************************************************************
    /* Multi-byte UTF-8 (exercises binary-parser code paths)
    /**********************************************************************
     */

    @Test
    void twoByteUtf8() throws Exception
    {
        // �� = U+00E9, encoded as 2 bytes in UTF-8
        String value = "caf\u00e9";
        for (int mode : ALL_BINARY_MODES) {
            _testUtf8Value(mode, value);
        }
    }

    @Test
    void threeByteUtf8() throws Exception
    {
        // ��� = U+4E2D, encoded as 3 bytes in UTF-8
        String value = "\u4e2d\u6587";
        for (int mode : ALL_BINARY_MODES) {
            _testUtf8Value(mode, value);
        }
    }

    @Test
    void surrogateRoundTrip() throws Exception
    {
        // ���� = U+1F600, encoded as 4 bytes in UTF-8, represented as surrogate pair
        // in Java: \uD83D\uDE00
        String value = "\uD83D\uDE00 emoji \uD83D\uDE00";
        for (int mode : ALL_BINARY_MODES) {
            _testUtf8Value(mode, value);
        }
    }

    @Test
    void surrogateAtOutputBufferBoundary() throws Exception
    {
        // Place a surrogate-pair character right at the output buffer boundary
        // to exercise the mid-surrogate flush path in _streamString.
        // Surrogate pair takes 2 Java chars; put one char before flush boundary
        // so the high surrogate falls exactly at position OUT_BUF_SIZE-1.
        String prefix = "x".repeat(OUT_BUF_SIZE - 1);
        String value = prefix + "\uD83D\uDE00" + "tail";
        for (int mode : ALL_BINARY_MODES) {
            _testUtf8Value(mode, value);
        }
    }

    private void _testUtf8Value(int mode, String value) throws Exception
    {
        String json = "[" + q(value) + "]";
        JsonParser p = createParser(JSON_FACTORY, mode, json);
        assertToken(JsonToken.START_ARRAY, p.nextToken());
        assertToken(JsonToken.VALUE_STRING, p.nextToken());

        Writer w = new StringWriter();
        long len = p.readString(w);
        assertEquals(value, w.toString(), "mode=" + mode);
        assertEquals((long) value.length(), len, "mode=" + mode);

        p.close();
    }

    /*
    /**********************************************************************
    /* Output-buffer boundary sizes
    /**********************************************************************
     */

    @Test
    void stringExactlyAtOutputBufferSize() throws Exception
    {
        // exactly 512 chars: buffer fills completely, then closing quote arrives
        _testStringOfLength(OUT_BUF_SIZE);
    }

    @Test
    void stringOneOverOutputBufferSize() throws Exception
    {
        _testStringOfLength(OUT_BUF_SIZE + 1);
    }

    @Test
    void stringTwoFullOutputBuffers() throws Exception
    {
        _testStringOfLength(OUT_BUF_SIZE * 2);
    }

    @Test
    void stringTwoFullOutputBuffersPlusOne() throws Exception
    {
        _testStringOfLength(OUT_BUF_SIZE * 2 + 1);
    }

    private void _testStringOfLength(int length) throws Exception
    {
        String value = "a".repeat(length);
        String json = "[" + q(value) + "]";
        for (int mode : ALL_MODES) {
            JsonParser p = createParser(JSON_FACTORY, mode, json);
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());

            Writer w = new StringWriter();
            long len = p.readString(w);
            assertEquals((long) length, len, "mode=" + mode + ", length=" + length);
            assertEquals(value, w.toString(), "mode=" + mode + ", length=" + length);

            p.close();
        }
    }

    /*
    /**********************************************************************
    /* Constraint enforcement at boundaries
    /**********************************************************************
     */

    @Test
    void constraintAtExactLimit() throws Exception
    {
        // A string of exactly maxLen chars must NOT throw
        for (int mode : ALL_MODES) {
            _testConstraintAtExactLimit(mode);
        }
    }

    private void _testConstraintAtExactLimit(int mode) throws Exception
    {
        final int maxLen = 1000;
        String value = "x".repeat(maxLen);
        JsonFactory f = JsonFactory.builder()
                .streamReadConstraints(StreamReadConstraints.builder().maxStringLength(maxLen).build())
                .build();

        JsonParser p = createParser(f, mode, "[" + q(value) + "]");
        assertToken(JsonToken.START_ARRAY, p.nextToken());
        assertToken(JsonToken.VALUE_STRING, p.nextToken());

        Writer w = new StringWriter();
        long len = p.readString(w);
        assertEquals((long) maxLen, len, "mode=" + mode);
        assertEquals(value, w.toString(), "mode=" + mode);

        p.close();
    }

    @Test
    void constraintOneLargerThanFlushBoundary() throws Exception
    {
        // maxLen set to OUT_BUF_SIZE - 1, so violation is detected at first flush
        for (int mode : ALL_MODES) {
            _testConstraintOneLargerThanFlushBoundary(mode);
        }
    }

    private void _testConstraintOneLargerThanFlushBoundary(int mode) throws Exception
    {
        final int maxLen = OUT_BUF_SIZE - 1;
        String value = "x".repeat(OUT_BUF_SIZE + 100); // clearly over limit
        JsonFactory f = JsonFactory.builder()
                .streamReadConstraints(StreamReadConstraints.builder().maxStringLength(maxLen).build())
                .build();

        JsonParser p = createParser(f, mode, "[" + q(value) + "]");
        assertToken(JsonToken.START_ARRAY, p.nextToken());
        assertToken(JsonToken.VALUE_STRING, p.nextToken());

        Writer w = new StringWriter();
        assertThrows(StreamConstraintsException.class, () -> p.readString(w), "mode=" + mode);

        p.close();
    }

    @Test
    void constraintExactlyAtFlushBoundary() throws Exception
    {
        // maxLen set to OUT_BUF_SIZE, so a string of OUT_BUF_SIZE+1 triggers it
        for (int mode : ALL_MODES) {
            _testConstraintExactlyAtFlushBoundary(mode);
        }
    }

    private void _testConstraintExactlyAtFlushBoundary(int mode) throws Exception
    {
        final int maxLen = OUT_BUF_SIZE;
        String value = "x".repeat(OUT_BUF_SIZE + 1);
        JsonFactory f = JsonFactory.builder()
                .streamReadConstraints(StreamReadConstraints.builder().maxStringLength(maxLen).build())
                .build();

        JsonParser p = createParser(f, mode, "[" + q(value) + "]");
        assertToken(JsonToken.START_ARRAY, p.nextToken());
        assertToken(JsonToken.VALUE_STRING, p.nextToken());

        Writer w = new StringWriter();
        assertThrows(StreamConstraintsException.class, () -> p.readString(w), "mode=" + mode);

        p.close();
    }

    /*
    /**********************************************************************
    /* Other token types
    /**********************************************************************
     */

    @Test
    void readStringOnPropertyName() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testReadStringOnPropertyName(mode);
        }
    }

    private void _testReadStringOnPropertyName(int mode) throws Exception
    {
        JsonParser p = createParser(JSON_FACTORY, mode, "{\"myKey\":\"myValue\"}");
        assertToken(JsonToken.START_OBJECT, p.nextToken());
        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());

        Writer w = new StringWriter();
        long len = p.readString(w);
        assertEquals("myKey", w.toString());
        assertEquals(5L, len);

        p.close();
    }

    @Test
    void readStringOnNumberTokens() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testReadStringOnNumberTokens(mode);
        }
    }

    private void _testReadStringOnNumberTokens(int mode) throws Exception
    {
        JsonParser p = createParser(JSON_FACTORY, mode, "[42, 3.14]");
        assertToken(JsonToken.START_ARRAY, p.nextToken());

        assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
        Writer w = new StringWriter();
        p.readString(w);
        assertEquals("42", w.toString());

        assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
        w = new StringWriter();
        p.readString(w);
        assertEquals("3.14", w.toString());

        p.close();
    }

    @Test
    void readStringOnNullToken() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testReadStringOnNullToken(mode);
        }
    }

    private void _testReadStringOnNullToken(int mode) throws Exception
    {
        JsonParser p = createParser(JSON_FACTORY, mode, "[null]");
        assertToken(JsonToken.START_ARRAY, p.nextToken());
        assertToken(JsonToken.VALUE_NULL, p.nextToken());

        Writer w = new StringWriter();
        long len = p.readString(w);
        assertEquals("null", w.toString());
        assertEquals(4L, len);

        p.close();
    }

    @Test
    void readStringBeforeFirstToken() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testReadStringBeforeFirstToken(mode);
        }
    }

    private void _testReadStringBeforeFirstToken(int mode) throws Exception
    {
        // No nextToken() called yet: current token is null, should return 0
        JsonParser p = createParser(JSON_FACTORY, mode, "[\"x\"]");
        Writer w = new StringWriter();
        long len = p.readString(w);
        assertEquals(0L, len);
        assertEquals("", w.toString());
        p.close();
    }

    /*
    /**********************************************************************
    /* Consuming semantics and sequential calls
    /**********************************************************************
     */

    @Test
    void consumingSemantics() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testConsumingSemantics(mode);
        }
    }

    private void _testConsumingSemantics(int mode) throws Exception
    {
        JsonParser p = createParser(JSON_FACTORY, mode, "[\"hello\",\"world\"]");
        assertToken(JsonToken.START_ARRAY, p.nextToken());

        // First value
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        Writer w = new StringWriter();
        long len = p.readString(w);
        assertEquals("hello", w.toString());
        assertEquals(5L, len);
        // getString() after readString() must return empty
        assertEquals("", p.getString());

        // Second value -- parser must have advanced past "hello"
        assertToken(JsonToken.VALUE_STRING, p.nextToken());
        w = new StringWriter();
        len = p.readString(w);
        assertEquals("world", w.toString());
        assertEquals(5L, len);

        assertToken(JsonToken.END_ARRAY, p.nextToken());
        p.close();
    }

    @Test
    void multipleStringsInArray() throws Exception
    {
        for (int mode : ALL_MODES) {
            _testMultipleStringsInArray(mode);
        }
    }

    private void _testMultipleStringsInArray(int mode) throws Exception
    {
        String[] values = {"first", "second", "third"};
        String json = "[\"first\",\"second\",\"third\"]";

        JsonParser p = createParser(JSON_FACTORY, mode, json);
        assertToken(JsonToken.START_ARRAY, p.nextToken());
        for (String expected : values) {
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            Writer w = new StringWriter();
            long len = p.readString(w);
            assertEquals(expected, w.toString(), "mode=" + mode);
            assertEquals((long) expected.length(), len, "mode=" + mode);
        }
        assertToken(JsonToken.END_ARRAY, p.nextToken());
        p.close();
    }

    /*
    /**********************************************************************
    /* Mixed content (ASCII + escapes + multi-byte)
    /**********************************************************************
     */

    @Test
    void mixedContent() throws Exception
    {
        // ASCII, escape sequences, and non-ASCII chars all in one string
        String expected = "Hello \n\t\u00e9 \u4e2d\u6587 \uD83D\uDE00 end";
        // Build the JSON manually so it round-trips correctly
        String json = "[\"Hello \\n\\t\\u00e9 \\u4e2d\\u6587 \\uD83D\\uDE00 end\"]";

        for (int mode : ALL_MODES) {
            JsonParser p = createParser(JSON_FACTORY, mode, json);
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());

            Writer w = new StringWriter();
            long len = p.readString(w);
            assertEquals(expected, w.toString(), "mode=" + mode);
            assertEquals((long) expected.length(), len, "mode=" + mode);

            p.close();
        }
    }

    @Test
    void longMixedContent() throws Exception
    {
        // Long string (> OUT_BUF_SIZE) with interspersed non-ASCII chars.
        // Control characters are excluded since q() doesn't escape them;
        // those code paths are covered by the dedicated escape tests.
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 50; i++) {
            sb.append("ASCII_block_");
            sb.append("\u00e9"); // 2-byte UTF-8
            sb.append("\u4e2d"); // 3-byte UTF-8
            sb.append("\uD83D\uDE00"); // surrogate pair (4-byte UTF-8)
        }
        String expected = sb.toString();
        // Use getString() to get the parser's own encoding of the value,
        // then verify readString() produces identical output.
        String json = "[" + q(expected) + "]";

        for (int mode : ALL_MODES) {
            JsonParser p = createParser(JSON_FACTORY, mode, json);
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());

            Writer w = new StringWriter();
            long len = p.readString(w);
            assertEquals(expected.length(), w.toString().length(), "mode=" + mode);
            assertEquals(expected, w.toString(), "mode=" + mode);
            assertEquals((long) expected.length(), len, "mode=" + mode);

            p.close();
        }
    }

    /*
    /**********************************************************************
    /* Throttled (input buffer boundary) mode
    /**********************************************************************
     */

    @Test
    void escapeSplitAcrossInputChunks() throws Exception
    {
        // In throttled mode (1 byte at a time), escape sequences will be split
        // across input loads. Verify correctness with all escape types.
        String json = "[\"\\n\\t\\r\\\"\\\\\\u00e9\"]";
        String expected = "\n\t\r\"\\\u00e9";

        // Use throttled stream mode (reads 1 byte at a time)
        try (JsonParser p = createParser(JSON_FACTORY, MODE_INPUT_STREAM_THROTTLED, json)) {
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
    
            Writer w = new StringWriter();
            long len = p.readString(w);
            assertEquals(expected, w.toString());
            assertEquals((long) expected.length(), len);
        }
    }

    @Test
    void longStringThrottled() throws Exception
    {
        // Long string in throttled mode: stresses input-buffer-boundary logic
        String value = "abcdefghij".repeat(200); // 2000 chars
        String json = "[" + q(value) + "]";

        try (JsonParser p = createParser(JSON_FACTORY, MODE_INPUT_STREAM_THROTTLED, json)) {
            assertToken(JsonToken.START_ARRAY, p.nextToken());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
    
            Writer w = new StringWriter();
            long len = p.readString(w);
            assertEquals(value, w.toString());
            assertEquals(2000L, len);
        }
    }
}