LongName1516Test.java

package tools.jackson.core.unittest.read;

import java.util.List;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.sym.PropertyNameMatcher;
import tools.jackson.core.unittest.*;
import tools.jackson.core.util.Named;

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

/**
 * Reproduction test for:
 * <a href="https://github.com/FasterXML/jackson-core/issues/1516">Issue #1516</a>
 * <p>
 * Buffer overflow bug in {@code UTF8StreamJsonParser._matchLongName()} that causes
 * {@code ArrayIndexOutOfBoundsException} when parsing JSON with long names
 * (longer than 64 characters).
 */
public class LongName1516Test
    extends JacksonCoreTestBase
{
    /**
     * Test for the exact case mentioned in issue #1516:
     * A 65-character name should not cause ArrayIndexOutOfBoundsException
     */
    @Test
    void longName65Characters() throws Exception
    {
        _testLongName65Characters(MODE_READER);
        _testLongName65Characters(MODE_INPUT_STREAM);
        _testLongName65Characters(MODE_INPUT_STREAM_THROTTLED);
        _testLongName65Characters(MODE_DATA_INPUT);
    }

    private void _testLongName65Characters(int mode) throws Exception
    {
        // 65 character name as mentioned in the issue
        String longName = "01234567890123456789012345678901234567890123456789012345678901234";
        assertEquals(65, longName.length(), "Name should be exactly 65 characters");

        String json = "{\"a\": \"123\", \"" + longName + "\": \"value\"}";

        try (JsonParser p = createParser(mode, json)) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
    
            // First property
            assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
            assertEquals("a", p.currentName());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            assertEquals("123", p.getString());
    
            // Second property with long name - this triggers the bug in 1516
            assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
            assertEquals(longName, p.currentName());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            assertEquals("value", p.getString());
    
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }
    }

    /**
     * Test various long name lengths to verify buffer expansion works correctly
     */
    @Test
    void longNamesVariousLengths() throws Exception
    {
        _testLongNamesVariousLengths(MODE_READER);
        _testLongNamesVariousLengths(MODE_INPUT_STREAM);
        _testLongNamesVariousLengths(MODE_INPUT_STREAM_THROTTLED);
        _testLongNamesVariousLengths(MODE_DATA_INPUT);
    }

    private void _testLongNamesVariousLengths(int mode) throws Exception
    {
        // Test names of various lengths that could trigger buffer boundary issues
        int[] lengths = { 60, 64, 65, 70, 80, 100, 128, 200 };

        for (int len : lengths) {
            StringBuilder nameB = new StringBuilder(len);
            for (int i = 0; i < len; i++) {
                nameB.append((char)('0' + (i % 10)));
            }

            String name = nameB.toString();
            String json = "{\"" + name + "\": 42}";

            try (JsonParser p = createParser(mode, json)) {
                assertToken(JsonToken.START_OBJECT, p.nextToken());
                assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
                assertEquals(name, p.currentName(),
                    "Failed for name length: " + len);
                assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
                assertEquals(42, p.getIntValue());
                assertToken(JsonToken.END_OBJECT, p.nextToken());
            }
        }
    }

    /**
     * Test multiple long names in the same document
     */
    @Test
    void multipleLongNames() throws Exception
    {
        _testMultipleLongNames(MODE_READER);
        _testMultipleLongNames(MODE_INPUT_STREAM);
        _testMultipleLongNames(MODE_INPUT_STREAM_THROTTLED);
        _testMultipleLongNames(MODE_DATA_INPUT);
    }

    private void _testMultipleLongNames(int mode) throws Exception
    {
        // Create multiple 65+ character names
        String name1 = "field1_" + "x".repeat(65);
        String name2 = "field2_" + "y".repeat(70);
        String name3 = "field3_" + "z".repeat(80);

        String json = "{\"" + name1 + "\": 1, \"" + name2 + "\": 2, \"" + name3 + "\": 3}";

        JsonParser p = createParser(mode, json);

        assertToken(JsonToken.START_OBJECT, p.nextToken());

        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals(name1, p.currentName());
        assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
        assertEquals(1, p.getIntValue());

        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals(name2, p.currentName());
        assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
        assertEquals(2, p.getIntValue());

        assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
        assertEquals(name3, p.currentName());
        assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
        assertEquals(3, p.getIntValue());

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

    /**
     * Test long names with UTF-8 multi-byte characters
     */
    @Test
    void longNamesWithUTF8() throws Exception
    {
        _testLongNamesWithUTF8(MODE_READER);
        _testLongNamesWithUTF8(MODE_INPUT_STREAM);
        _testLongNamesWithUTF8(MODE_INPUT_STREAM_THROTTLED);
        _testLongNamesWithUTF8(MODE_DATA_INPUT);
    }

    private void _testLongNamesWithUTF8(int mode) throws Exception
    {
        // 65+ character name with UTF-8 characters
        String name = "field_\u00E9\u00F1\u00FC_" + "a".repeat(60);
        assertTrue(name.length() >= 65, "Name should be at least 65 characters");

        String json = "{\"" + name + "\": \"test\"}";

        // Convert to UTF-8 bytes
        byte[] jsonBytes = utf8Bytes(json);

        try (JsonParser p = createParser(mode, jsonBytes)) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
            assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
            assertEquals(name, p.currentName());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            assertEquals("test", p.getString());
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }
    }

    /**
     * Test using PropertyNameMatcher with long names (65 characters)
     * This is the code path that triggers the bug in _matchLongName()
     */
    @Test
    void longNameWithMatcher65Chars() throws Exception
    {
        _testLongNameWithMatcher65Chars(MODE_READER);
        _testLongNameWithMatcher65Chars(MODE_INPUT_STREAM);
        _testLongNameWithMatcher65Chars(MODE_INPUT_STREAM_THROTTLED);
        _testLongNameWithMatcher65Chars(MODE_DATA_INPUT);
    }

    private void _testLongNameWithMatcher65Chars(int mode) throws Exception
    {
        JsonFactory f = newStreamFactory();

        // 65 character name as mentioned in the issue
        String longName = "01234567890123456789012345678901234567890123456789012345678901234";
        assertEquals(65, longName.length(), "Name should be exactly 65 characters");

        String json = "{\"a\": \"123\", \"" + longName + "\": \"value\"}";

        // Create matcher with both names
        PropertyNameMatcher matcher = f.constructNameMatcher(
            List.of(Named.fromString("a"), Named.fromString(longName)),
            false);

        try (JsonParser p = createParser(f, mode, json)) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
    
            assertEquals(0, p.nextNameMatch(matcher));
            assertToken(JsonToken.PROPERTY_NAME, p.currentToken());
            assertEquals("a", p.currentName());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            assertEquals("123", p.getString());
    
            // Second property with long name - this should trigger the bug in _matchLongName()
            assertEquals(1, p.nextNameMatch(matcher));
            assertToken(JsonToken.PROPERTY_NAME, p.currentToken());
            assertEquals(longName, p.currentName());
            assertToken(JsonToken.VALUE_STRING, p.nextToken());
            assertEquals("value", p.getString());
    
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }
    }

    /**
     * Test using PropertyNameMatcher with multiple long names of various lengths
     */
    @Test
    void multipleNamesWithMatcher() throws Exception
    {
        _testMultipleNamesWithMatcher(MODE_READER);
        _testMultipleNamesWithMatcher(MODE_INPUT_STREAM);
        _testMultipleNamesWithMatcher(MODE_INPUT_STREAM_THROTTLED);
        _testMultipleNamesWithMatcher(MODE_DATA_INPUT);
    }

    private void _testMultipleNamesWithMatcher(int mode) throws Exception
    {
        JsonFactory f = newStreamFactory();

        // Names of different lengths, including 65+ characters
        String field1 = "shortField";
        String field2 = "field64chars_" + "x".repeat(52); // 64 chars
        String field3 = "field65chars_" + "y".repeat(52); // 65 chars
        String field4 = "field80chars_" + "z".repeat(67); // 80 chars

        String json = "{\"" + field1 + "\": 1, \"" + field2 + "\": 2, \""
                     + field3 + "\": 3, \"" + field4 + "\": 4}";

        PropertyNameMatcher matcher = f.constructNameMatcher(
            List.of(Named.fromString(field1), Named.fromString(field2),
                   Named.fromString(field3), Named.fromString(field4)),
            false);

        try (JsonParser p = createParser(f, mode, json)) {
            assertToken(JsonToken.START_OBJECT, p.nextToken());
    
            assertEquals(0, p.nextNameMatch(matcher));
            assertEquals(field1, p.currentName());
            assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
            assertEquals(1, p.getIntValue());
    
            // Name 2 (64 chars)
            assertEquals(1, p.nextNameMatch(matcher));
            assertEquals(field2, p.currentName());
            assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
            assertEquals(2, p.getIntValue());
    
            // Name 3 (65 chars) - triggers buffer boundary
            assertEquals(2, p.nextNameMatch(matcher));
            assertEquals(field3, p.currentName());
            assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
            assertEquals(3, p.getIntValue());
    
            // Name 4 (80 chars) - should also work
            assertEquals(3, p.nextNameMatch(matcher));
            assertEquals(field4, p.currentName());
            assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
            assertEquals(4, p.getIntValue());
    
            assertToken(JsonToken.END_OBJECT, p.nextToken());
        }
    }
}