ControlCharBetweenTokens1795Test.java
package tools.jackson.core.unittest.read;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
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.json.JsonReadFeature;
import tools.jackson.core.unittest.JacksonCoreTestBase;
// [core#1795]: control characters between tokens must be rejected by all parsers,
// including the `DataInput`-based one
class ControlCharBetweenTokens1795Test
extends JacksonCoreTestBase
{
private final JsonFactory JSON_F = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_JAVA_COMMENTS)
.build();
private final JsonFactory RS_F = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_RS_CONTROL_CHAR)
.build();
private final JsonFactory COMMENTS_F = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_JAVA_COMMENTS)
.enable(JsonReadFeature.ALLOW_YAML_COMMENTS)
.build();
@Test
void ctrlCharBetweenArrayValues() throws Exception {
_verifyRejected("[1,\u0001 2]");
}
@Test
void ctrlCharBeforeRootValue() throws Exception {
_verifyRejected("\u0001[1]");
}
@Test
void ctrlCharAfterRootValue() throws Exception {
_verifyRejected("[1]\u0001");
}
@Test
void ctrlCharAfterRootScalar() throws Exception {
for (String doc : new String[] { "123\u0001", "1\u0001 2", "1.5\u0001", "-1\u0001" }) {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(JSON_F, mode, doc)) {
_consume(p, mode);
fail("Should not pass for mode " + mode + ", doc: " + doc);
} catch (StreamReadException e) {
verifyException(e, "CTRL-CHAR, code 1");
}
}
}
}
@Test
void ctrlCharWithNextName() throws Exception {
for (String doc : new String[] { "\u0001{\"a\":1}", "{\"a\":1,\u0001\"b\":2}" }) {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(JSON_F, mode, doc)) {
_consumeWithNextName(p);
fail("Should not pass for mode " + mode + ", doc: " + doc);
} catch (StreamReadException e) {
verifyException(e, "Illegal character ((CTRL-CHAR, code 1))");
}
}
}
}
@Test
void ctrlCharBeforeColon() throws Exception {
_verifyRejected("{\"a\"\u0001:1}");
}
@Test
void ctrlCharAfterColon() throws Exception {
_verifyRejected("{\"a\":\u0001 1}");
}
@Test
void ctrlCharAfterComment() throws Exception {
_verifyRejected("[1,/* c */\u0001 2]");
_verifyRejected("{\"a\"/* c */\u0001:1}");
}
@Test
void validWhiteSpaceStillAccepted() throws Exception {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(JSON_F, mode,
" \t\r\n{ \"a\" \t:\r\n 1 , \"b\"\n:\t2}\r\n ")) {
_consume(p, mode);
}
}
}
// Like the other parsers, Record Separator is accepted where token skipping starts
// (between root values, but also e.g. after '[' or before ',' and ']'); not after
// comma or colon, nor directly after a root-level number or a comment
@Test
void recordSeparatorAllowedBetweenRootValues() throws Exception {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(RS_F, mode, "[1]\u001E[2]\u001E")) {
_consume(p, mode);
}
}
}
@Test
void recordSeparatorAllowedWithNextName() throws Exception {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(RS_F, mode, "{\"a\":1}\u001E{\"b\":2}\u001E")) {
assertEquals(2, _consumeWithNextName(p));
}
}
}
// CR-LF must count as a single linefeed with all parsers
@Test
void crlfLineCounting() throws Exception {
final String doc = "[\r\n1,\r2,\n3,\r\n/* a\r\n b */ 4,\r\n// c\r\n5,\r\n# d\r\n6,"
+ "{\"x\"\r\n:\r\n7}]\r\n8\r\n9 ";
final int[] expLines = { 1, 2, 3, 4, 6, 8, 10, 10, 10, 12, 12, 12, 13, 14 };
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(COMMENTS_F, mode, doc)) {
for (int i = 0; i < expLines.length; ++i) {
assertNotNull(p.nextToken());
assertEquals(expLines[i], p.currentTokenLocation().getLineNr(),
"Token #" + i + " (" + p.currentToken() + "), mode " + mode);
}
assertNull(p.nextToken());
}
}
}
@Test
void recordSeparatorRejectedAfterRootNumberOrComment() throws Exception {
final JsonFactory f = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_RS_CONTROL_CHAR)
.enable(JsonReadFeature.ALLOW_JAVA_COMMENTS)
.build();
for (String doc : new String[] { "1\u001E2", "[1]/* c */\u001E[2]" }) {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(f, mode, doc)) {
_consume(p, mode);
fail("Should not pass for mode " + mode + ", doc: " + doc);
} catch (StreamReadException e) {
verifyException(e, "CTRL-CHAR, code 30");
}
}
}
}
// Comment (and white space after it) at the end of content is valid
@Test
void commentAtEndOfInput() throws Exception {
for (String doc : new String[] { "[1] // c", "[1] // c\r\n ", "[1] # c\n", "[1]\r",
"[1] /* c */", "[1] /* c */ \n", "1 /* c */" }) {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(COMMENTS_F, mode, doc)) {
_consume(p, mode);
}
}
}
}
@Test
void eofInsideValueWhiteSpaceOrComment() throws Exception {
for (String doc : new String[] { "[1] /* c", "[1] /", "[1, // c",
"[1, // c\n", "[1, /* c */ ", "[1, # c\r", "{\"a\" // c\n", "{\"a\": /* c */\r\n",
"[1,\r", "[1,", "[1, ", "{\"a\"", "{\"a\" ", "{\"a\":", "{\"a\": ", "{\"a\":\t\n" }) {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(COMMENTS_F, mode, doc)) {
_consume(p, mode);
fail("Should not pass for mode " + mode + ", doc: " + doc);
} catch (StreamReadException e) {
verifyException(e, "Unexpected end-of-input");
}
}
}
}
@Test
void truncatedUtf8InCommentAtEnd() throws Exception {
final byte[] doc = new byte[] { '[', '1', ']', ' ', '/', '/', ' ', (byte) 0xC3 };
for (int mode : ALL_BINARY_MODES) {
try (JsonParser p = createParser(COMMENTS_F, mode, doc)) {
_consume(p, mode);
fail("Should not pass for mode " + mode);
} catch (StreamReadException e) {
verifyException(e, "Unexpected end-of-input");
}
}
}
// Value following property name must report its own start row
@Test
void valueLocationAfterName() throws Exception {
final JsonFactory f = JsonFactory.builder()
.enable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS)
.build();
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(f, mode, "{\"a\":\n'x\ny',\"b\"\r\n:\r\n[1]}")) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertEquals("a", p.nextName());
assertEquals(1, p.currentTokenLocation().getLineNr(), "mode " + mode);
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals(2, p.currentTokenLocation().getLineNr(), "mode " + mode);
assertEquals("b", p.nextName());
assertEquals(3, p.currentTokenLocation().getLineNr(), "mode " + mode);
// via `nextStringValue()` which handles deferred value separately
assertNull(p.nextStringValue());
assertToken(JsonToken.START_ARRAY, p.currentToken());
assertEquals(5, p.currentTokenLocation().getLineNr(), "mode " + mode);
}
}
}
@Test
void recordSeparatorRejectedInsideValue() throws Exception {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(RS_F, mode, "[1,\u001E2]")) {
_consume(p, mode);
fail("Should not pass for mode " + mode);
} catch (StreamReadException e) {
verifyException(e, "Illegal character ((CTRL-CHAR, code 30))");
}
}
}
private void _verifyRejected(String doc) throws Exception {
for (int mode : ALL_MODES) {
try (JsonParser p = createParser(JSON_F, mode, doc)) {
_consume(p, mode);
fail("Should not pass for mode " + mode + ", doc: " + doc);
} catch (StreamReadException e) {
verifyException(e, "Illegal character ((CTRL-CHAR, code 1))");
}
}
}
private int _consumeWithNextName(JsonParser p) throws Exception {
int names = 0;
while (true) {
if (p.nextName() != null) {
++names;
} else if (p.currentToken() == null) {
return names;
}
}
}
private void _consume(JsonParser p, int mode) throws Exception {
int tokens = 0;
while (p.nextToken() != null) {
++tokens;
}
assertTrue(tokens > 0, "No tokens for mode " + mode);
}
}