EscapedSurrogateInFieldName1744Test.java
package tools.jackson.core.unittest.read;
import org.junit.jupiter.api.Test;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.core.unittest.JacksonCoreTestBase;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Tests for [jackson-core#1744]: JSON-escaped surrogate pairs in field names
* must be decoded by all blocking parsers, including {@code UTF8DataInputJsonParser}.
*/
class EscapedSurrogateInFieldName1744Test extends JacksonCoreTestBase
{
private final JsonFactory FACTORY = newStreamFactory();
private final JsonFactory APOS_FACTORY = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
.build();
// U+1F600 GRINNING FACE
private static final String SMILEY = "\uD83D\uDE00";
private static final String ESC_SMILEY = "\\uD83D\\uDE00";
@Test
void escapedSurrogatePairInFieldName() throws Exception
{
for (int mode : ALL_MODES) {
// vary prefix length to cover all quad alignments
for (String prefix : new String[] { "", "a", "ab", "abc", "abcd", "\u00e9", "\u20ac" }) {
_testName(FACTORY, mode, '"', prefix + ESC_SMILEY + "xyz", prefix + SMILEY + "xyz");
}
_testName(FACTORY, mode, '"', ESC_SMILEY + ESC_SMILEY, SMILEY + SMILEY);
_testName(FACTORY, mode, '"', "\\ud834\\udd1e", "\uD834\uDD1E");
}
}
@Test
void escapedSurrogatePairInAposFieldName() throws Exception
{
for (int mode : ALL_MODES) {
for (String prefix : new String[] { "", "a", "ab", "abc", "abcd" }) {
_testName(APOS_FACTORY, mode, '\'', prefix + ESC_SMILEY + "xyz", prefix + SMILEY + "xyz");
}
}
}
// Long enough (> 64 bytes as UTF-8) to require growing quad buffer
@Test
void longNameWithEscapedSurrogatePairs() throws Exception
{
StringBuilder esc = new StringBuilder();
StringBuilder exp = new StringBuilder();
for (int i = 0; i < 40; ++i) {
esc.append(ESC_SMILEY);
exp.append(SMILEY);
}
for (int mode : ALL_MODES) {
for (String prefix : new String[] { "", "a", "ab", "abc" }) {
_testName(FACTORY, mode, '"', prefix + esc, prefix + exp);
_testName(APOS_FACTORY, mode, '\'', prefix + esc, prefix + exp);
}
}
}
// Backslash-escaped supplementary character cannot be decoded as single char:
// must be rejected, not truncated to 16 bits (U+10027 would become apostrophe)
@Test
void backslashEscapedSupplementaryCharRejected() throws Exception
{
final JsonFactory anyEscape = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER)
.build();
final String apos = new String(Character.toChars(0x10027));
for (int mode : ALL_MODES) {
for (String doc : new String[] {
"{\"\\" + SMILEY + "\":1}",
"[\"\\" + SMILEY + "\"]",
"{\"" + ESC_SMILEY + "\\" + SMILEY + "\":1}",
}) {
_testBroken(FACTORY, mode, doc, "Unrecognized character escape");
_testBroken(anyEscape, mode, doc, "Unrecognized character escape");
}
_testBroken(APOS_FACTORY, mode, "{\"\\" + apos + "\":1}", "Unrecognized character escape");
_testBroken(APOS_FACTORY, mode, "[\"\\" + apos + "\"]", "Unrecognized character escape");
}
}
// Same for invalid UTF-8 after backslash (only possible with byte input)
@Test
void backslashEscapedInvalidUTF8() throws Exception
{
final JsonFactory f = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER)
.build();
// beyond U+10FFFF
final int[] tooBig = { 0xF4, 0x90, 0x80, 0x80 };
// CESU-8 style encoded high surrogate U+D83D
final int[] cesu = { 0xED, 0xA0, 0xBD };
for (int mode : ALL_BINARY_MODES) {
// (3.x already rejects this as invalid UTF-8 when decoding)
_testBroken(f, mode, utf8Bytes("{\"\\", tooBig, "\":1}"), "Unrecognized character escape", "Invalid UTF-8");
_testBroken(f, mode, utf8Bytes("[\"\\", tooBig, "\"]"), "Unrecognized character escape", "Invalid UTF-8");
_testBroken(f, mode, utf8Bytes("{\"\\", cesu, "\\uDE00\":1}"), "Unrecognized character escape");
_testBroken(f, mode, utf8Bytes("[\"\\", cesu, "\\uDE00\"]"), "Unrecognized character escape");
}
}
@Test
void brokenSurrogatePairInFieldName() throws Exception
{
for (int mode : ALL_MODES) {
// high surrogate not followed by escape
_testBroken(FACTORY, mode, "{\"\\uD83Dx\":1}", "Broken surrogate pair");
// high surrogate right before closing quote
_testBroken(FACTORY, mode, "{\"\\uD83D\":1}", "Broken surrogate pair");
// high surrogate followed by non-low-surrogate escape
_testBroken(FACTORY, mode, "{\"\\uD83D\\u0041\":1}", "Broken surrogate pair");
// high surrogate followed by another high surrogate
_testBroken(FACTORY, mode, "{\"\\uD83D\\uD83D\":1}", "Broken surrogate pair");
// lone low surrogate
_testBroken(FACTORY, mode, "{\"\\uDE00\":1}", "Unexpected low surrogate");
_testBroken(APOS_FACTORY, mode, "{'\\uD83Dx':1}", "Broken surrogate pair");
_testBroken(APOS_FACTORY, mode, "{'\\uDE00':1}", "Unexpected low surrogate");
// hex-escaped high surrogate followed by raw low surrogate
_testBroken(FACTORY, mode, "{\"\\uD83D\uDE00\":1}", "Broken surrogate pair");
}
}
@Test
void eofWithinSurrogatePairInFieldName() throws Exception
{
for (int mode : ALL_MODES) {
_testBroken(FACTORY, mode, "{\"\\uD83D", "Unexpected end-of-input");
_testBroken(FACTORY, mode, "{\"\\uD83D\\", "Unexpected end-of-input");
_testBroken(FACTORY, mode, "{\"\\uD83D\\uDE", "Unexpected end-of-input");
}
}
// Truncated escape must be reported as JsonEOFException by all backends
@Test
void eofWithinEscape() throws Exception
{
for (int mode : ALL_MODES) {
_testBroken(FACTORY, mode, "{\"\\", "Unexpected end-of-input");
_testBroken(FACTORY, mode, "{\"\\uD8", "Unexpected end-of-input");
_testBroken(FACTORY, mode, "[\"\\", "Unexpected end-of-input");
_testBroken(FACTORY, mode, "[\"\\u00", "Unexpected end-of-input");
}
}
private void _testName(JsonFactory f, int mode, char q, String escName, String expName)
throws Exception
{
String doc = "{" + q + escName + q + ":1}";
try (JsonParser p = createParser(f, mode, doc)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals(expName, p.currentName(), "mode " + mode);
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertToken(JsonToken.END_OBJECT, p.nextToken());
}
}
private void _testBroken(JsonFactory f, int mode, String doc, String... expMsgs)
throws Exception
{
_testBroken(createParser(f, mode, doc), mode, expMsgs);
}
private void _testBroken(JsonFactory f, int mode, byte[] doc, String... expMsgs)
throws Exception
{
_testBroken(createParser(f, mode, doc), mode, expMsgs);
}
private void _testBroken(JsonParser parser, int mode, String... expMsgs) throws Exception
{
try (JsonParser p = parser) {
while (p.nextToken() != null) {
if (p.currentToken() == JsonToken.VALUE_STRING) {
p.getText();
}
}
fail("Should not pass, mode " + mode);
} catch (StreamReadException e) {
verifyException(e, expMsgs);
}
}
}