AsyncRootNumberSeparationTest.java
package tools.jackson.core.unittest.json.async;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.JsonToken;
import tools.jackson.core.unittest.async.AsyncTestBase;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.core.json.JsonReadFeature;
import tools.jackson.core.unittest.testutil.AsyncReaderWrapper;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
/**
* Tests that a root-level number must be followed by white space (or end-of-input),
* as with blocking parsers, and that malformed numbers are reported
* regardless of where input chunk boundaries fall
* (see [core#1506] for the original root-level issue).
*/
class AsyncRootNumberSeparationTest extends AsyncTestBase
{
private final JsonFactory F = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS)
.enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS)
.enable(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS)
.build();
private final static int[] BYTES_PER_READ = { 1, 2, 3, 5, 100 };
// Must fail on the very first token: no number returned before the error
@Test
void missingSeparatorAfterRootNumber() throws Exception
{
for (String json : new String[] {
"12-3 ", "1.5-2 ", "1e5-1 ", "0-1 ", "-0-1 ", "-.5-.5 ",
"1[2] ", "1{} ", "1\"a\" ", "1true ", "1.5true ", "1.5false ", "1e5.5 ",
"0.5x ", "1,2 ", "1]", "1}"
}) {
_testFailsOnFirstToken(json, "Expected space separating root-level values");
}
}
// Non-ASCII character reported decoded, if fully buffered; lead byte if not
@Test
void nonAsciiAfterRootNumber() throws Exception
{
_testFailsOnFirstToken("1\u00e9 ", "Expected space separating root-level values");
AsyncReaderWrapper p = _parser("1\u00e9 ", 100, false);
try {
p.nextToken();
fail("Should not pass");
} catch (StreamReadException e) {
verifyException(e, "('\u00e9' (code 233))");
} finally {
p.close();
}
p = _parser("1.5\u20ac ", 100, true);
try {
p.nextToken();
fail("Should not pass");
} catch (StreamReadException e) {
verifyException(e, "(code 8364 / 0x20ac)");
} finally {
p.close();
}
}
// As with blocking parsers, unexpected character just ends the number
@Test
void unexpectedCharAfterRootNumber() throws Exception
{
for (String json : new String[] { "1.5f ", "1.5D ", "0a ", "0]" }) {
_testFailsOnFirstToken(json, "Expected space separating root-level values");
}
}
// [core#679]: second decimal point fails on the number itself
@Test
void secondDecimalPoint() throws Exception
{
_testFailsOnFirstToken("1.5.5 ", "more than one decimal point");
_testFailsOnSecondToken("[1.5.5]", "more than one decimal point");
_testFailsOnSecondToken("[-.5.5]", "more than one decimal point");
}
// [core#707]: hex literal without feature enabled fails on the number itself
@Test
void hexWithoutFeature() throws Exception
{
for (String json : new String[] { "0x ", "-0x " }) {
_testFailsOnFirstToken(json, "hexadecimal number literals require enabling");
}
_testFailsOnSecondToken("[0x]", "hexadecimal number literals require enabling");
_testFailsOnSecondToken("[-0x]", "hexadecimal number literals require enabling");
}
// Inside Arrays/Objects, as with blocking parsers, unexpected character ends
// the number, then fails as missing comma (regardless of chunk boundaries)
@Test
void unexpectedCharAfterNumberInArray() throws Exception
{
_testFailsAfterNumber("[1.5f]", JsonToken.VALUE_NUMBER_FLOAT, "1.5");
_testFailsAfterNumber("[1.5D]", JsonToken.VALUE_NUMBER_FLOAT, "1.5");
_testFailsAfterNumber("[1e5f]", JsonToken.VALUE_NUMBER_FLOAT, "1e5");
}
// But decimal point must still be followed by a digit
@Test
void decimalPointWithoutDigitInArray() throws Exception
{
_testFailsOnSecondToken("[1..5]", "Decimal point not followed by a digit");
_testFailsOnSecondToken("[1.f]", "Decimal point not followed by a digit");
}
// Non-ASCII byte ending fraction reported same regardless of chunking
@Test
void nonAsciiAfterDecimalPointInArray() throws Exception
{
_testFailsOnSecondToken("[1.\u00e9]", "(code 195)");
}
@Test
void validRootSeparators() throws Exception
{
for (String json : new String[] { "1 2", "1\n2", "1\t2", "1\r2", "1\r\n2", "1 2 " }) {
for (int bytesPerRead : BYTES_PER_READ) {
for (boolean bb : new boolean[] { false, true }) {
AsyncReaderWrapper p = _parser(json, bytesPerRead, bb);
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(1, p.getIntValue());
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(2, p.getIntValue());
assertNull(p.nextToken());
p.close();
}
}
}
}
// RS (JSON Text Sequences) accepted after root-level number, if enabled
@Test
void recordSeparatorAfterRootNumber() throws Exception
{
final JsonFactory rsF = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_RS_CONTROL_CHAR)
.build();
for (String json : new String[] { "1\u001E2", "1.5\u001E2", "1e2\u001E2", "0\u001E2", "-0\u001E2" }) {
for (int bytesPerRead : BYTES_PER_READ) {
for (boolean bb : new boolean[] { false, true }) {
byte[] doc = json.getBytes(StandardCharsets.UTF_8);
AsyncReaderWrapper p = bb ? asyncForByteBuffer(rsF, bytesPerRead, doc, 0)
: asyncForBytes(rsF, bytesPerRead, doc, 0);
assertTrue(p.nextToken().isNumeric());
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(2, p.getIntValue());
assertNull(p.nextToken());
p.close();
}
}
}
// but not without feature enabled
_testFailsOnFirstToken("1\u001E2", "Expected space separating root-level values");
}
@Test
void validRootFloats() throws Exception
{
for (int bytesPerRead : BYTES_PER_READ) {
for (boolean bb : new boolean[] { false, true }) {
AsyncReaderWrapper p = _parser("1.5 .5\n-.25e1 0", bytesPerRead, bb);
assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
assertEquals(1.5, p.getDoubleValue());
assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
assertEquals(0.5, p.getDoubleValue());
assertToken(JsonToken.VALUE_NUMBER_FLOAT, p.nextToken());
assertEquals(-2.5, p.getDoubleValue());
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(0, p.getIntValue());
assertNull(p.nextToken());
p.close();
}
}
}
private void _testFailsOnFirstToken(String json, String expMsg) throws Exception
{
for (int bytesPerRead : BYTES_PER_READ) {
for (boolean bb : new boolean[] { false, true }) {
AsyncReaderWrapper p = _parser(json, bytesPerRead, bb);
try {
JsonToken t = p.nextToken();
fail("Should not pass for '" + json + "' (" + bytesPerRead
+ " bytes/read); got token " + t + " ('" + p.currentText() + "')");
} catch (StreamReadException e) {
verifyException(e, expMsg);
} finally {
p.close();
}
}
}
}
private void _testFailsOnSecondToken(String json, String expMsg) throws Exception
{
for (int bytesPerRead : BYTES_PER_READ) {
for (boolean bb : new boolean[] { false, true }) {
AsyncReaderWrapper p = _parser(json, bytesPerRead, bb);
assertToken(JsonToken.START_ARRAY, p.nextToken());
try {
JsonToken t = p.nextToken();
fail("Should not pass for '" + json + "' (" + bytesPerRead
+ " bytes/read); got token " + t + " ('" + p.currentText() + "')");
} catch (StreamReadException e) {
verifyException(e, expMsg);
} finally {
p.close();
}
}
}
}
private void _testFailsAfterNumber(String json, JsonToken expToken, String expText) throws Exception
{
for (int bytesPerRead : BYTES_PER_READ) {
for (boolean bb : new boolean[] { false, true }) {
AsyncReaderWrapper p = _parser(json, bytesPerRead, bb);
assertToken(JsonToken.START_ARRAY, p.nextToken());
assertToken(expToken, p.nextToken());
assertEquals(expText, p.currentText());
try {
JsonToken t = p.nextToken();
fail("Should not pass for '" + json + "' (" + bytesPerRead
+ " bytes/read); got token " + t);
} catch (StreamReadException e) {
verifyException(e, "was expecting comma to separate Array entries");
} finally {
p.close();
}
}
}
}
private AsyncReaderWrapper _parser(String json, int bytesPerRead, boolean bb)
throws Exception
{
byte[] doc = json.getBytes(StandardCharsets.UTF_8);
return bb ? asyncForByteBuffer(F, bytesPerRead, doc, 0)
: asyncForBytes(F, bytesPerRead, doc, 0);
}
}