HexEscapeNonAscii1787Test.java
package tools.jackson.core.unittest.read;
import java.io.ByteArrayOutputStream;
import org.junit.jupiter.api.Test;
import tools.jackson.core.*;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.json.JsonFactory;
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#1787]: non-ASCII chars must not be accepted as hex
* digits of a {@code \}{@code u} escape (they used to alias to ASCII ones with
* the same low byte, e.g. U+2042 to 'B', with char-based input).
*/
class HexEscapeNonAscii1787Test extends JacksonCoreTestBase
{
private final JsonFactory FACTORY = newStreamFactory();
@Test
void nonAsciiHexDigitsRejected() throws Exception
{
// U+2042 -> 'B', U+0141 -> 'A', U+0130 -> '0'
_testBroken("\\u\u2042\u2042\u2042\u2042", "'\u2042' (code 8258 / 0x2042)");
_testBroken("\\u\u0141BCD", "'\u0141' (code 321 / 0x141)");
_testBroken("\\u00\u01300", "'\u0130' (code 304 / 0x130)");
_testBroken("\\u\uD83D\uDE00", null);
}
// Invalid or truncated UTF-8 in hex escape must still be reported as bad hex digit
// (lead byte as-is), not as UTF-8 decoding or end-of-input problem
@Test
void invalidUtf8HexDigitsRejected() throws Exception
{
_testBrokenBytes(new int[] { 0x80 }, "(CTRL-CHAR, code 128)");
_testBrokenBytes(new int[] { 0xFF }, "'\u00FF' (code 255)");
// bad continuation byte
_testBrokenBytes(new int[] { 0xC3, 'A' }, "'\u00C3' (code 195)");
_testBrokenBytes(new int[] { 0xE2, 0x81, 'B' }, "'\u00E2' (code 226)");
// invalid lead bytes (overlong 2-byte, beyond U+10FFFF)
_testBrokenBytes(new int[] { 0xC0, 0x80 }, "'\u00C0' (code 192)");
_testBrokenBytes(new int[] { 0xF5, 0x80, 0x80, 0x80 }, "'\u00F5' (code 245)");
// overlong 3-byte; surrogate
_testBrokenBytes(new int[] { 0xE0, 0x80, 0x80 }, "'\u00E0' (code 224)");
_testBrokenBytes(new int[] { 0xED, 0xA0, 0x80 }, "'\u00ED' (code 237)");
}
@Test
void truncatedUtf8HexDigitRejected() throws Exception
{
for (int mode : ALL_BINARY_MODES) {
byte[] doc = _bytes("[\"\\u", new int[] { 0xE2, 0x81 }, "");
try (JsonParser p = createParser(FACTORY, mode, doc)) {
assertToken(JsonToken.START_ARRAY, p.nextToken());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
p.getText();
fail("Should not pass for mode " + mode);
} catch (StreamReadException e) {
verifyException(e, "Unexpected character ('\u00E2' (code 226)): expected a hex-digit");
}
}
}
// Error location must not depend on whether bad hex digit is multi-byte
@Test
void errorLocationForMultiByteChar() throws Exception
{
for (int mode : new int[] { MODE_INPUT_STREAM, MODE_INPUT_STREAM_THROTTLED }) {
TokenStreamLocation asciiLoc = _errorLocation(mode, "[\"\\uZZZZ\"]");
TokenStreamLocation multiLoc = _errorLocation(mode, "[\"\\u\u2042\u2042\u2042\u2042\"]");
assertEquals(asciiLoc.getByteOffset(), multiLoc.getByteOffset());
assertEquals(asciiLoc.getColumnNr(), multiLoc.getColumnNr());
}
}
@Test
void asciiHexDigitsAccepted() throws Exception
{
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(FACTORY, mode, "{\"\\u00e9\":\"\\uABcd\"}")) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("\u00e9", p.currentName());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("\uABCD", p.getText());
assertToken(JsonToken.END_OBJECT, p.nextToken());
}
}
}
private void _testBroken(String escape, String expCharDesc) throws Exception
{
for (int mode : ALL_MODES) {
for (String doc : new String[] { "[\"" + escape + "\"]", "{\"" + escape + "\":1}" }) {
try (JsonParser p = createParser(FACTORY, mode, doc)) {
while (p.nextToken() != null) {
p.getText();
}
fail("Should not pass for mode " + mode + ", doc: " + doc);
} catch (StreamReadException e) {
verifyException(e, "expected a hex-digit for character escape sequence");
// best-effort decoding needs whole char buffered (DataInput: never decoded)
if (expCharDesc != null && mode != MODE_INPUT_STREAM_THROTTLED
&& mode != MODE_DATA_INPUT) {
verifyException(e, "Unexpected character (" + expCharDesc + ")");
}
}
}
}
}
private void _testBrokenBytes(int[] badBytes, String expCharDesc) throws Exception
{
for (int mode : ALL_BINARY_MODES) {
for (byte[] doc : new byte[][] { _bytes("[\"\\u", badBytes, "\"]"),
_bytes("{\"\\u", badBytes, "\":1}") }) {
try (JsonParser p = createParser(FACTORY, mode, doc)) {
while (p.nextToken() != null) {
p.getText();
}
fail("Should not pass for mode " + mode);
} catch (StreamReadException e) {
verifyException(e, "Unexpected character (" + expCharDesc
+ "): expected a hex-digit for character escape sequence");
}
}
}
}
private TokenStreamLocation _errorLocation(int mode, String doc) throws Exception
{
try (JsonParser p = createParser(FACTORY, mode, doc)) {
assertToken(JsonToken.START_ARRAY, p.nextToken());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
p.getText();
fail("Should not pass for mode " + mode + ", doc: " + doc);
return null;
} catch (StreamReadException e) {
verifyException(e, "expected a hex-digit");
return e.getLocation();
}
}
static byte[] _bytes(String prefix, int[] raw, String suffix) throws Exception
{
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(prefix.getBytes("UTF-8"));
for (int b : raw) {
bytes.write(b);
}
bytes.write(suffix.getBytes("UTF-8"));
return bytes.toByteArray();
}
}