SupplementaryCharDesc1728Test.java

package tools.jackson.core.unittest.json;

import java.util.function.Supplier;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JsonParser;
import tools.jackson.core.ObjectReadContext;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.unittest.JacksonCoreTestBase;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

// [core#1728]: unexpected-character messages must quote the supplementary
// code point, not the 16-bit truncation of it.
class SupplementaryCharDesc1728Test extends JacksonCoreTestBase
{
    // U+1F648 SEE-NO-EVIL MONKEY. Low 16 bits are U+F648, a different character.
    private static final int SEE_NO_EVIL = 0x1F648;

    private static final byte[] SEE_NO_EVIL_UTF8 = new byte[] {
        (byte) 0xF0, (byte) 0x9F, (byte) 0x99, (byte) 0x88
    };

    // U+1D400 MATHEMATICAL BOLD CAPITAL A: a supplementary Java identifier char.
    // Low 16 bits are U+D400, a Hangul syllable (also identifier char).
    private static final int MATH_BOLD_A = 0x1D400;

    private static final byte[] MATH_BOLD_A_UTF8 = new byte[] {
        (byte) 0xF0, (byte) 0x9D, (byte) 0x90, (byte) 0x80
    };

    @Test
    void unexpectedSupplementaryValueQuotesFullCodePoint() throws Exception
    {
        _assertFullCodePoint(() -> JSON_FACTORY.createParser(ObjectReadContext.empty(),
                SEE_NO_EVIL_UTF8), 1);
        for (int mode : ALL_BINARY_MODES) {
            _assertFullCodePoint(() -> createParser(mode, SEE_NO_EVIL_UTF8), 1);
        }
    }

    @Test
    void unexpectedSupplementaryPropertyNameQuotesFullCodePoint() throws Exception
    {
        final byte[] doc = _concat(new byte[] { '{' }, SEE_NO_EVIL_UTF8);

        _assertFullCodePoint(() -> JSON_FACTORY.createParser(ObjectReadContext.empty(), doc), 2);
        for (int mode : ALL_BINARY_MODES) {
            _assertFullCodePoint(() -> createParser(mode, doc), 2);
        }
    }

    // Unexpected supplementary char right after a matched keyword
    @Test
    void unexpectedSupplementaryAfterTokenQuotesFullCodePoint() throws Exception
    {
        final byte[] doc = _concat(_concat("true".getBytes("UTF-8"), SEE_NO_EVIL_UTF8),
                new byte[] { ' ' }); // trailing space for DataInput

        _assertFullCodePoint(() -> JSON_FACTORY.createParser(ObjectReadContext.empty(), doc), 2);
        for (int mode : ALL_BINARY_MODES) {
            _assertFullCodePoint(() -> createParser(mode, doc), 2);
        }
    }

    // Supplementary identifier char included in "Unrecognized token" text:
    // at token start, directly after keyword, and later in token
    @Test
    void invalidTokenIncludesFullSupplementaryCodePoint() throws Exception
    {
        final String letter = new String(Character.toChars(MATH_BOLD_A));
        for (String prefix : new String[] { "", "true", "truex" }) {
            final byte[] doc = _concat(_concat(prefix.getBytes("UTF-8"), MATH_BOLD_A_UTF8),
                    new byte[] { ' ' }); // trailing space for DataInput
            final String expected = "Unrecognized token '" + prefix + letter + "'";

            _assertInvalidToken(() -> JSON_FACTORY.createParser(ObjectReadContext.empty(), doc),
                    expected);
            for (int mode : ALL_BINARY_MODES) {
                _assertInvalidToken(() -> createParser(mode, doc), expected);
            }
        }
    }

    // Lead bytes that would decode above U+10FFFF must be reported as invalid UTF-8
    @Test
    void codePointAboveMaxReportedAsInvalidUtf8() throws Exception
    {
        _assertError(new byte[] { (byte) 0xF5, (byte) 0x80, (byte) 0x80, (byte) 0x80, ' ' },
                "Invalid UTF-8 4-byte sequence (0xF5 0x80 ...): code point exceeds U+10FFFF");
        _assertError(new byte[] { (byte) 0xF4, (byte) 0x90, (byte) 0x80, (byte) 0x80, ' ' },
                "Invalid UTF-8 4-byte sequence (0xF4 0x90 ...): code point exceeds U+10FFFF");
        // but U+10FFFF itself is valid
        _assertError(new byte[] { (byte) 0xF4, (byte) 0x8F, (byte) 0xBF, (byte) 0xBF, ' ' },
                "Unexpected character ('" + new String(Character.toChars(0x10FFFF))
                + "' (code 1114111 / 0x10ffff)");
    }

    private void _assertError(byte[] doc, String expected)
    {
        assertThatThrownBy(() -> _readTokens(
                () -> JSON_FACTORY.createParser(ObjectReadContext.empty(), doc), 1))
                .isInstanceOf(StreamReadException.class)
                .hasMessageContaining(expected);
        for (int mode : ALL_BINARY_MODES) {
            assertThatThrownBy(() -> _readTokens(() -> createParser(mode, doc), 1))
                    .isInstanceOf(StreamReadException.class)
                    .hasMessageContaining(expected);
        }
    }

    private static void _assertInvalidToken(Supplier<JsonParser> parserSupplier, String expected)
    {
        assertThatThrownBy(() -> _readTokens(parserSupplier, 2))
                .isInstanceOf(StreamReadException.class)
                .hasMessageContaining(expected)
                .hasMessageNotContaining(String.valueOf((char) MATH_BOLD_A));
    }

    private static void _readTokens(Supplier<JsonParser> parserSupplier, int tokens)
    {
        try (JsonParser p = parserSupplier.get()) {
            for (int i = 0; i < tokens; ++i) {
                p.nextToken();
            }
        }
    }

    private static byte[] _concat(byte[] a, byte[] b)
    {
        final byte[] result = new byte[a.length + b.length];
        System.arraycopy(a, 0, result, 0, a.length);
        System.arraycopy(b, 0, result, a.length, b.length);
        return result;
    }

    private static void _assertFullCodePoint(Supplier<JsonParser> parserSupplier, int tokens)
    {
        final String quoted = "'" + new String(Character.toChars(SEE_NO_EVIL)) + "'";
        final String truncated = "'" + (char) SEE_NO_EVIL + "'";

        assertThatThrownBy(() -> _readTokens(parserSupplier, tokens))
                .isInstanceOf(StreamReadException.class)
                .hasMessageContaining("Unexpected character (" + quoted)
                .hasMessageContaining("code " + SEE_NO_EVIL)
                .hasMessageContaining("0x" + Integer.toHexString(SEE_NO_EVIL))
                .hasMessageNotContaining(truncated);
    }
}