From 717cd6b3db1574b9cb4ac2d69c19f0f10e162150 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Sat, 30 Dec 2023 19:33:42 -0800 Subject: [PATCH] Cleave of failing #1180 tests from passing #1173 tests --- .../fasterxml/jackson/core/JsonLocation.java | 10 +- .../read/loc}/LocationOfError1173Test.java | 39 +--- .../failing/LocationOfError1180Test.java | 204 ++++++++++++++++++ 3 files changed, 215 insertions(+), 38 deletions(-) rename src/test/java/com/fasterxml/jackson/{failing/read => core/read/loc}/LocationOfError1173Test.java (89%) create mode 100644 src/test/java/com/fasterxml/jackson/failing/LocationOfError1180Test.java diff --git a/src/main/java/com/fasterxml/jackson/core/JsonLocation.java b/src/main/java/com/fasterxml/jackson/core/JsonLocation.java index 589410e3d3..ce95f5cc05 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonLocation.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonLocation.java @@ -152,10 +152,16 @@ public Object getSourceRef() { public int getLineNr() { return _lineNr; } /** - * Access for getting column position of this location, if available. + * Access for getting column offset of this location, if available. * Note that column position is typically not available for binary formats. + * Note: this returns an offset that is in units of input, so for {@code byte}-based + * input sources (like {@link java.io.InputStream}) this does not take into + * account multi-byte characters: one logical character can be 1, 2 or 3 bytes long. + * To calculate column position in characters either {@code char}-based input + * source (like {@link java.io.Reader}) needs to be used, or content needs to be + * explicitly decoded. * - * @return Column position of the location (1-based), if available; {@code -1} if not. + * @return Column offset of the location (1-based), if available; {@code -1} if not. */ public int getColumnNr() { return _columnNr; } diff --git a/src/test/java/com/fasterxml/jackson/failing/read/LocationOfError1173Test.java b/src/test/java/com/fasterxml/jackson/core/read/loc/LocationOfError1173Test.java similarity index 89% rename from src/test/java/com/fasterxml/jackson/failing/read/LocationOfError1173Test.java rename to src/test/java/com/fasterxml/jackson/core/read/loc/LocationOfError1173Test.java index aea27cc9a6..175e28bb70 100644 --- a/src/test/java/com/fasterxml/jackson/failing/read/LocationOfError1173Test.java +++ b/src/test/java/com/fasterxml/jackson/core/read/loc/LocationOfError1173Test.java @@ -1,4 +1,4 @@ -package com.fasterxml.jackson.failing.read; +package com.fasterxml.jackson.core.read.loc; import java.io.*; import java.nio.charset.StandardCharsets; @@ -21,8 +21,8 @@ import static org.junit.Assert.assertTrue; /** - * Tests that the {@link JsonLocation} attached to a thrown {@link JsonProcessingException} due to invalid json points - * to the correct character. + * Tests that the {@link JsonLocation} attached to a thrown {@link StreamReadException} + * due to invalid JSON points to the correct character. */ public class LocationOfError1173Test { @@ -200,39 +200,6 @@ public JsonParser createParser(String input) throws Exception 1, 2 ), - new InvalidJson( - "Incorrect case for false literal", - "{\"isThisValidJson\": FALSE}", - 24, - 24, - 1, - 25 - ), - new InvalidJson( - "Incorrect case for true literal", - "{\"shouldYouAvoidWritingJsonLikeThis\": TRUE}", - 41, - 41, - 1, - 42 - ), - new InvalidJson( - "Incorrect case for null literal", - "{\"licensePlate\": NULL}", - 20, - 20, - 1, - 21 - ), - new InvalidJson( - "Invalid JSON with raw unicode character", - // javac will parse the 3-byte unicode control sequence, it will be passed to the parser as a raw unicode character - a2q("{'validJson':'\u274c','right', 'here'}"), - 26, - 24, - 1, - 25 - ), new InvalidJson( "Error in middle of line for multiline input", // missing comma delimiter between properties two and three diff --git a/src/test/java/com/fasterxml/jackson/failing/LocationOfError1180Test.java b/src/test/java/com/fasterxml/jackson/failing/LocationOfError1180Test.java new file mode 100644 index 0000000000..44b18a8d62 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/failing/LocationOfError1180Test.java @@ -0,0 +1,204 @@ +package com.fasterxml.jackson.failing; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.core.async.ByteArrayFeeder; +import com.fasterxml.jackson.core.exc.StreamReadException; + +import static com.fasterxml.jackson.core.BaseTest.a2q; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests that the {@link JsonLocation} attached to a thrown {@link StreamReadException} + * due to invalid JSON points to the correct character. + */ +public class LocationOfError1180Test +{ + static final JsonFactory JSON_F = new JsonFactory(); + + /** Represents the different parser backends */ + public enum ParserVariant + { + BYTE_ARRAY( + (String input) -> JSON_F.createParser(input.getBytes(StandardCharsets.UTF_8)), + true, // supports byte offsets in reported location + false, // supports character offsets in reported location + true // supports column numbers in reported location + ), + CHAR_ARRAY( + (String input) -> JSON_F.createParser(input.toCharArray()), + false, + true, + true + ), + ASYNC( + (String input) -> { + JsonParser parser = JSON_F.createNonBlockingByteArrayParser(); + ByteArrayFeeder feeder = (ByteArrayFeeder) parser.getNonBlockingInputFeeder(); + assertTrue(feeder.needMoreInput()); + + byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8); + feeder.feedInput(inputBytes, 0, inputBytes.length); + feeder.endOfInput(); + + return parser; + }, + true, + false, + true + ) + ; + + ParserVariant( + ParserGenerator parserGenerator, + boolean supportsByteOffset, + boolean supportsCharOffset, + boolean supportsColumnNr + ) + { + _parserGenerator = parserGenerator; + + this.supportsByteOffset = supportsByteOffset; + this.supportsCharOffset = supportsCharOffset; + this.supportsColumnNr = supportsColumnNr; + } + + public JsonParser createParser(String input) throws Exception + { + return _parserGenerator.createParser(input); + } + + private final ParserGenerator _parserGenerator; + public final boolean supportsByteOffset; + public final boolean supportsCharOffset; + public final boolean supportsColumnNr; + } + + /** Collection of differing invalid JSON input cases to test */ + private static final List INVALID_JSON_CASES = Arrays.asList( + new InvalidJson( + "Incorrect case for false literal", + "{\"isThisValidJson\": FALSE}", + 24, + 24, + 1, + 25 + ), + new InvalidJson( + "Incorrect case for true literal", + "{\"shouldYouAvoidWritingJsonLikeThis\": TRUE}", + 41, + 41, + 1, + 42 + ), + new InvalidJson( + "Incorrect case for null literal", + "{\"licensePlate\": NULL}", + 20, + 20, + 1, + 21 + ), + // NOTE: to be removed, eventually + new InvalidJson( + "Invalid JSON with raw unicode character", + // javac will parse the 3-byte unicode control sequence, it will be passed to the parser as a raw unicode character + a2q("{'validJson':'\u274c','right', 'here'}"), + 26, + 24, + 1, + 25 + ) + ); + + @ParameterizedTest + @MethodSource("_generateTestData") + public void testParserBackendWithInvalidJson(ParserVariant variant, InvalidJson invalidJson) + throws Exception + { + try (JsonParser parser = variant.createParser(invalidJson.input)) + { + StreamReadException e = Assertions.assertThrows( + StreamReadException.class, + () -> { + // Blindly advance the parser through the end of input + while (parser.nextToken() != null) {} + } + ); + + JsonLocation location = e.getLocation(); + assertEquals(invalidJson.lineNr, location.getLineNr()); + final String msg = e.getOriginalMessage(); + + if (variant.supportsByteOffset) + { + assertEquals("Incorrect byte offset (for '"+msg+"')", + invalidJson.byteOffset, location.getByteOffset()); + } + if (variant.supportsCharOffset) + { + assertEquals("Incorrect char offset (for '"+msg+"')", + invalidJson.charOffset, location.getCharOffset()); + } + if (variant.supportsColumnNr) + { + assertEquals("Incorrect column (for '"+msg+"')", + invalidJson.columnNr, location.getColumnNr()); + } + } + } + + private static Stream _generateTestData() + { + return Arrays.stream(ParserVariant.values()) + .flatMap(parserVariant -> INVALID_JSON_CASES.stream().map( + invalidJson -> Arguments.of(parserVariant, invalidJson) + )); + } + + @FunctionalInterface + public interface ParserGenerator + { + JsonParser createParser(String input) throws Exception; + } + + static class InvalidJson + { + InvalidJson(String name, String input, int byteOffset, int charOffset, + int lineNr, int columnNr) + { + _name = name; + + this.input = input; + this.byteOffset = byteOffset; + this.charOffset = charOffset; + this.lineNr = lineNr; + this.columnNr = columnNr; + } + + @Override + public String toString() + { + return _name; + } + + private final String _name; + public final String input; + public final int byteOffset; + public final int charOffset; + public final int lineNr; + public final int columnNr; + } +}