diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index df0b2994b..ded271e17 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -75,19 +75,31 @@ public JSONArray() { } /** - * Construct a JSONArray from a JSONTokener. + * Constructs a JSONArray from a JSONTokener. + *

+ * This constructor reads the JSONTokener to parse a JSON array. It uses the default JSONParserConfiguration. * - * @param x - * A JSONTokener - * @throws JSONException - * If there is a syntax error. + * @param x A JSONTokener + * @throws JSONException If there is a syntax error. */ public JSONArray(JSONTokener x) throws JSONException { + this(x, new JSONParserConfiguration()); + } + + /** + * Constructs a JSONArray from a JSONTokener and a JSONParserConfiguration. + * JSONParserConfiguration contains strictMode turned off (false) by default. + * + * @param x A JSONTokener instance from which the JSONArray is constructed. + * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser. + * @throws JSONException If a syntax error occurs during the construction of the JSONArray. + */ + public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException { this(); if (x.nextClean() != '[') { throw x.syntaxError("A JSONArray text must start with '['"); } - + char nextChar = x.nextClean(); if (nextChar == 0) { // array is unclosed. No ']' found, instead EOF @@ -101,27 +113,34 @@ public JSONArray(JSONTokener x) throws JSONException { this.myArrayList.add(JSONObject.NULL); } else { x.back(); - this.myArrayList.add(x.nextValue()); + this.myArrayList.add(x.nextValue(jsonParserConfiguration)); } switch (x.nextClean()) { - case 0: - // array is unclosed. No ']' found, instead EOF - throw x.syntaxError("Expected a ',' or ']'"); - case ',': - nextChar = x.nextClean(); - if (nextChar == 0) { + case 0: // array is unclosed. No ']' found, instead EOF throw x.syntaxError("Expected a ',' or ']'"); - } - if (nextChar == ']') { + case ',': + nextChar = x.nextClean(); + if (nextChar == 0) { + // array is unclosed. No ']' found, instead EOF + throw x.syntaxError("Expected a ',' or ']'"); + } + if (nextChar == ']') { + return; + } + x.back(); + break; + case ']': + if (jsonParserConfiguration.isStrictMode()) { + nextChar = x.nextClean(); + if (nextChar != 0) { + throw x.syntaxError("invalid character found after end of array: " + nextChar); + } + } + return; - } - x.back(); - break; - case ']': - return; - default: - throw x.syntaxError("Expected a ',' or ']'"); + default: + throw x.syntaxError("Expected a ',' or ']'"); } } } @@ -138,7 +157,19 @@ public JSONArray(JSONTokener x) throws JSONException { * If there is a syntax error. */ public JSONArray(String source) throws JSONException { - this(new JSONTokener(source)); + this(new JSONTokener(source), new JSONParserConfiguration()); + } + + /** + * Constructs a JSONArray from a source JSON text and a JSONParserConfiguration. + * + * @param source A string that begins with [ (left bracket) and + * ends with ]  (right bracket). + * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser. + * @throws JSONException If there is a syntax error. + */ + public JSONArray(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException { + this(new JSONTokener(source), jsonParserConfiguration); } /** @@ -367,7 +398,7 @@ public Number getNumber(int index) throws JSONException { /** * Get the enum value associated with an index. - * + * * @param * Enum Type * @param clazz @@ -555,7 +586,7 @@ public String join(String separator) throws JSONException { if (len == 0) { return ""; } - + StringBuilder sb = new StringBuilder( JSONObject.valueToString(this.myArrayList.get(0))); @@ -869,7 +900,7 @@ public Integer optIntegerObject(int index, Integer defaultValue) { /** * Get the enum value associated with a key. - * + * * @param * Enum Type * @param clazz @@ -884,7 +915,7 @@ public > E optEnum(Class clazz, int index) { /** * Get the enum value associated with a key. - * + * * @param * Enum Type * @param clazz @@ -917,8 +948,8 @@ public > E optEnum(Class clazz, int index, E defaultValue) } /** - * Get the optional BigInteger value associated with an index. The - * defaultValue is returned if there is no value for the index, or if the + * Get the optional BigInteger value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the * value is not a number and cannot be converted to a number. * * @param index @@ -933,8 +964,8 @@ public BigInteger optBigInteger(int index, BigInteger defaultValue) { } /** - * Get the optional BigDecimal value associated with an index. The - * defaultValue is returned if there is no value for the index, or if the + * Get the optional BigDecimal value associated with an index. The + * defaultValue is returned if there is no value for the index, or if the * value is not a number and cannot be converted to a number. If the value * is float or double, the {@link BigDecimal#BigDecimal(double)} * constructor will be used. See notes on the constructor for conversion @@ -1103,7 +1134,7 @@ public Number optNumber(int index, Number defaultValue) { if (val instanceof Number){ return (Number) val; } - + if (val instanceof String) { try { return JSONObject.stringToNumber((String) val); @@ -1180,7 +1211,7 @@ public JSONArray put(Collection value) { public JSONArray put(double value) throws JSONException { return this.put(Double.valueOf(value)); } - + /** * Append a float value. This increases the array's length by one. * @@ -1435,19 +1466,19 @@ public JSONArray put(int index, Object value) throws JSONException { * * @param collection * A Collection. - * @return this. + * @return this. */ public JSONArray putAll(Collection collection) { this.addAll(collection, false); return this; } - + /** * Put an Iterable's elements in to the JSONArray. * * @param iter * An Iterable. - * @return this. + * @return this. */ public JSONArray putAll(Iterable iter) { this.addAll(iter, false); @@ -1459,7 +1490,7 @@ public JSONArray putAll(Iterable iter) { * * @param array * A JSONArray. - * @return this. + * @return this. */ public JSONArray putAll(JSONArray array) { // directly copy the elements from the source array to this one @@ -1474,7 +1505,7 @@ public JSONArray putAll(JSONArray array) { * @param array * Array. If the parameter passed is null, or not an array or Iterable, an * exception will be thrown. - * @return this. + * @return this. * * @throws JSONException * If not an array, JSONArray, Iterable or if an value is non-finite number. @@ -1485,9 +1516,9 @@ public JSONArray putAll(Object array) throws JSONException { this.addAll(array, false); return this; } - + /** - * Creates a JSONPointer using an initialization string and tries to + * Creates a JSONPointer using an initialization string and tries to * match it to an item within this JSONArray. For example, given a * JSONArray initialized with this document: *

@@ -1495,7 +1526,7 @@ public JSONArray putAll(Object array) throws JSONException {
      *     {"b":"c"}
      * ]
      * 
- * and this JSONPointer string: + * and this JSONPointer string: *
      * "/0/b"
      * 
@@ -1508,9 +1539,9 @@ public JSONArray putAll(Object array) throws JSONException { public Object query(String jsonPointer) { return query(new JSONPointer(jsonPointer)); } - + /** - * Uses a user initialized JSONPointer and tries to + * Uses a user initialized JSONPointer and tries to * match it to an item within this JSONArray. For example, given a * JSONArray initialized with this document: *
@@ -1518,7 +1549,7 @@ public Object query(String jsonPointer) {
      *     {"b":"c"}
      * ]
      * 
- * and this JSONPointer: + * and this JSONPointer: *
      * "/0/b"
      * 
@@ -1531,11 +1562,11 @@ public Object query(String jsonPointer) { public Object query(JSONPointer jsonPointer) { return jsonPointer.queryFrom(this); } - + /** * Queries and returns a value from this object using {@code jsonPointer}, or * returns null if the query fails due to a missing key. - * + * * @param jsonPointer the string representation of the JSON pointer * @return the queried value or {@code null} * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax @@ -1543,11 +1574,11 @@ public Object query(JSONPointer jsonPointer) { public Object optQuery(String jsonPointer) { return optQuery(new JSONPointer(jsonPointer)); } - + /** * Queries and returns a value from this object using {@code jsonPointer}, or * returns null if the query fails due to a missing key. - * + * * @param jsonPointer The JSON pointer * @return the queried value or {@code null} * @throws IllegalArgumentException if {@code jsonPointer} has invalid syntax @@ -1667,11 +1698,11 @@ public String toString() { /** * Make a pretty-printed JSON text of this JSONArray. - * + * *

If

 {@code indentFactor > 0}
and the {@link JSONArray} has only * one element, then the array will be output on a single line: *
{@code [1]}
- * + * *

If an array has 2 or more elements, then it will be output across * multiple lines:

{@code
      * [
@@ -1683,7 +1714,7 @@ public String toString() {
      * 

* Warning: This method assumes that the data structure is acyclical. * - * + * * @param indentFactor * The number of spaces to add to each level of indentation. * @return a printable, displayable, transmittable representation of the @@ -1717,11 +1748,11 @@ public Writer write(Writer writer) throws JSONException { /** * Write the contents of the JSONArray as JSON text to a writer. - * + * *

If

{@code indentFactor > 0}
and the {@link JSONArray} has only * one element, then the array will be output on a single line: *
{@code [1]}
- * + * *

If an array has 2 or more elements, then it will be output across * multiple lines:

{@code
      * [
@@ -1947,7 +1978,7 @@ private void addAll(Object array, boolean wrap, int recursionDepth, JSONParserCo
                     "JSONArray initial value should be a string or collection or array.");
         }
     }
-    
+
     /**
      * Create a new JSONException in a common format for incorrect conversions.
      * @param idx index of the item
diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java
index 26a68c6dc..642e96703 100644
--- a/src/main/java/org/json/JSONObject.java
+++ b/src/main/java/org/json/JSONObject.java
@@ -220,12 +220,12 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration
         for (;;) {
             c = x.nextClean();
             switch (c) {
-            case 0:
-                throw x.syntaxError("A JSONObject text must end with '}'");
-            case '}':
-                return;
-            default:
-                key = x.nextSimpleValue(c).toString();
+                case 0:
+                    throw x.syntaxError("A JSONObject text must end with '}'");
+                case '}':
+                    return;
+                default:
+                    key = x.nextSimpleValue(c, jsonParserConfiguration).toString();
             }
 
             // The key is followed by ':'.
@@ -244,7 +244,7 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration
                     throw x.syntaxError("Duplicate key \"" + key + "\"");
                 }
 
-                Object value = x.nextValue();
+                Object value = x.nextValue(jsonParserConfiguration);
                 // Only add value if non-null
                 if (value != null) {
                     this.put(key, value);
@@ -1247,7 +1247,7 @@ public BigDecimal optBigDecimal(String key, BigDecimal defaultValue) {
     static BigDecimal objectToBigDecimal(Object val, BigDecimal defaultValue) {
         return objectToBigDecimal(val, defaultValue, true);
     }
-    
+
     /**
      * @param val value to convert
      * @param defaultValue default value to return is the conversion doesn't work or is null.
diff --git a/src/main/java/org/json/JSONParserConfiguration.java b/src/main/java/org/json/JSONParserConfiguration.java
index 190daeb88..ad0d7fb72 100644
--- a/src/main/java/org/json/JSONParserConfiguration.java
+++ b/src/main/java/org/json/JSONParserConfiguration.java
@@ -4,11 +4,25 @@
  * Configuration object for the JSON parser. The configuration is immutable.
  */
 public class JSONParserConfiguration extends ParserConfiguration {
+
+    /** Original Configuration of the JSON Parser. */
+    public static final JSONParserConfiguration ORIGINAL = new JSONParserConfiguration();
+
+    /** Original configuration of the JSON Parser except that values are kept as strings. */
+    public static final JSONParserConfiguration KEEP_STRINGS = new JSONParserConfiguration().withKeepStrings(true);
+
     /**
      * Used to indicate whether to overwrite duplicate key or not.
      */
     private boolean overwriteDuplicateKey;
 
+    /**
+     * This flag, when set to true, instructs the parser to throw a JSONException if it encounters an invalid character
+     * immediately following the final ']' character in the input. This is useful for ensuring strict adherence to the
+     * JSON syntax, as any characters after the final closing bracket of a JSON array are considered invalid.
+     */
+    private boolean strictMode;
+
     /**
      * Configuration with the default values.
      */
@@ -58,6 +72,24 @@ public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwrite
         return clone;
     }
 
+
+    /**
+     * Sets the strict mode configuration for the JSON parser.
+     * 

+ * When strict mode is enabled, the parser will throw a JSONException if it encounters an invalid character + * immediately following the final ']' character in the input. This is useful for ensuring strict adherence to the + * JSON syntax, as any characters after the final closing bracket of a JSON array are considered invalid. + * + * @param mode a boolean value indicating whether strict mode should be enabled or not + * @return a new JSONParserConfiguration instance with the updated strict mode setting + */ + public JSONParserConfiguration withStrictMode(final boolean mode) { + JSONParserConfiguration clone = this.clone(); + clone.strictMode = mode; + + return clone; + } + /** * The parser's behavior when meeting duplicate keys, controls whether the parser should * overwrite duplicate keys or not. @@ -67,4 +99,18 @@ public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwrite public boolean isOverwriteDuplicateKey() { return this.overwriteDuplicateKey; } + + + /** + * Retrieves the current strict mode setting of the JSON parser. + *

+ * Strict mode, when enabled, instructs the parser to throw a JSONException if it encounters an invalid character + * immediately following the final ']' character in the input. This ensures strict adherence to the JSON syntax, as + * any characters after the final closing bracket of a JSON array are considered invalid. + * + * @return the current strict mode setting. True if strict mode is enabled, false otherwise. + */ + public boolean isStrictMode() { + return this.strictMode; + } } diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index b8808bb4f..078e01620 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -284,13 +284,14 @@ public char nextClean() throws JSONException { * Backslash processing is done. The formal JSON format does not * allow strings in single quotes, but an implementation is allowed to * accept them. + * If strictMode is true, this implementation will not accept unbalanced quotes (e.g will not accept "test') * @param quote The quoting character, either * " (double quote) or * ' (single quote). - * @return A String. - * @throws JSONException Unterminated string. + * @return A String. + * @throws JSONException Unterminated string or unbalanced quotes if strictMode == true. */ - public String nextString(char quote) throws JSONException { + public String nextString(char quote, boolean strictMode) throws JSONException { char c; StringBuilder sb = new StringBuilder(); for (;;) { @@ -338,11 +339,21 @@ public String nextString(char quote) throws JSONException { throw this.syntaxError("Illegal escape. Escape sequence \\" + c + " is not valid."); } break; - default: - if (c == quote) { - return sb.toString(); - } - sb.append(c); + default: + if (strictMode && c == '\"' && quote != c) { + throw this.syntaxError(String.format( + "Field contains unbalanced quotes. Starts with %s but ends with double quote.", quote)); + } + + if (strictMode && c == '\'' && quote != c) { + throw this.syntaxError(String.format( + "Field contains unbalanced quotes. Starts with %s but ends with single quote.", quote)); + } + + if (c == quote) { + return sb.toString(); + } + sb.append(c); } } } @@ -397,51 +408,103 @@ public String nextTo(String delimiters) throws JSONException { /** - * Get the next value. The value can be a Boolean, Double, Integer, - * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. - * @throws JSONException If syntax error. + * Get the next value. The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. * * @return An object. + * @throws JSONException If syntax error. */ public Object nextValue() throws JSONException { + return nextValue(new JSONParserConfiguration()); + } + + /** + * Get the next value. The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. The strictMode parameter controls the behavior of the method when parsing the value. + * + * @param jsonParserConfiguration which carries options such as strictMode, these methods will + * strictly adhere to the JSON syntax, throwing a JSONException for any deviations. + * @return An object. + * @throws JSONException If syntax error. + */ + public Object nextValue(JSONParserConfiguration jsonParserConfiguration) throws JSONException { char c = this.nextClean(); switch (c) { - case '{': - this.back(); - try { - return new JSONObject(this); - } catch (StackOverflowError e) { - throw new JSONException("JSON Array or Object depth too large to process.", e); - } - case '[': - this.back(); - try { - return new JSONArray(this); - } catch (StackOverflowError e) { - throw new JSONException("JSON Array or Object depth too large to process.", e); - } + case '{': + this.back(); + try { + return new JSONObject(this, jsonParserConfiguration); + } catch (StackOverflowError e) { + throw new JSONException("JSON Array or Object depth too large to process.", e); + } + case '[': + this.back(); + try { + return new JSONArray(this); + } catch (StackOverflowError e) { + throw new JSONException("JSON Array or Object depth too large to process.", e); + } + default: + return nextSimpleValue(c, jsonParserConfiguration); } - return nextSimpleValue(c); } - Object nextSimpleValue(char c) { - String string; + /** + * This method is used to get a JSONObject from the JSONTokener. The strictMode parameter controls the behavior of + * the method when parsing the JSONObject. + * + * @param jsonParserConfiguration which carries options such as strictMode, these methods will + * strictly adhere to the JSON syntax, throwing a JSONException for any deviations. + * deviations. + * @return A JSONObject which is the next value in the JSONTokener. + * @throws JSONException If the JSONObject or JSONArray depth is too large to process. + */ + private JSONObject getJsonObject(JSONParserConfiguration jsonParserConfiguration) { + try { + return new JSONObject(this, jsonParserConfiguration); + } catch (StackOverflowError e) { + throw new JSONException("JSON Array or Object depth too large to process.", e); + } + } - switch (c) { - case '"': - case '\'': - return this.nextString(c); + /** + * This method is used to get a JSONArray from the JSONTokener. + * + * @return A JSONArray which is the next value in the JSONTokener. + * @throws JSONException If the JSONArray depth is too large to process. + */ + private JSONArray getJsonArray() { + try { + return new JSONArray(this); + } catch (StackOverflowError e) { + throw new JSONException("JSON Array or Object depth too large to process.", e); } + } - /* - * Handle unquoted text. This could be the values true, false, or - * null, or it can be a number. An implementation (such as this one) - * is allowed to also accept non-standard forms. - * - * Accumulate characters until we reach the end of the text or a - * formatting character. - */ + Object nextSimpleValue(char c, JSONParserConfiguration jsonParserConfiguration) { + boolean strictMode = jsonParserConfiguration.isStrictMode(); + if(strictMode && c == '\''){ + throw this.syntaxError("Single quote wrap not allowed in strict mode"); + } + + if (c == '"' || c == '\'') { + return this.nextString(c, strictMode); + } + + return parsedUnquotedText(c, strictMode); + } + + /** + * Parses unquoted text from the JSON input. This could be the values true, false, or null, or it can be a number. + * Non-standard forms are also accepted. Characters are accumulated until the end of the text or a formatting + * character is reached. + * + * @param c The starting character. + * @return The parsed object. + * @throws JSONException If the parsed string is empty. + */ + private Object parsedUnquotedText(char c, boolean strictMode) { StringBuilder sb = new StringBuilder(); while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { sb.append(c); @@ -451,13 +514,37 @@ Object nextSimpleValue(char c) { this.back(); } - string = sb.toString().trim(); - if ("".equals(string)) { + String string = sb.toString().trim(); + + if (strictMode) { + boolean isBooleanOrNumeric = checkIfValueIsBooleanOrNumeric(string); + + if (isBooleanOrNumeric) { + return string; + } + + throw new JSONException(String.format("Value is not surrounded by quotes: %s", string)); + } + + if (string.isEmpty()) { throw this.syntaxError("Missing value"); } return JSONObject.stringToValue(string); } + private boolean checkIfValueIsBooleanOrNumeric(Object valueToValidate) { + String stringToValidate = valueToValidate.toString(); + if (stringToValidate.equals("true") || stringToValidate.equals("false")) { + return true; + } + + try { + Double.parseDouble(stringToValidate); + return true; + } catch (NumberFormatException e) { + return false; + } + } /** * Skip characters until the next character is the requested character. diff --git a/src/test/java/org/json/junit/JSONParserConfigurationTest.java b/src/test/java/org/json/junit/JSONParserConfigurationTest.java index 509b98879..a1838a4ee 100644 --- a/src/test/java/org/json/junit/JSONParserConfigurationTest.java +++ b/src/test/java/org/json/junit/JSONParserConfigurationTest.java @@ -1,14 +1,24 @@ package org.json.junit; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONParserConfiguration; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class JSONParserConfigurationTest { + private static final String TEST_SOURCE = "{\"key\": \"value1\", \"key\": \"value2\"}"; @Test(expected = JSONException.class) @@ -19,16 +29,162 @@ public void testThrowException() { @Test public void testOverwrite() { JSONObject jsonObject = new JSONObject(TEST_SOURCE, - new JSONParserConfiguration().withOverwriteDuplicateKey(true)); + new JSONParserConfiguration().withOverwriteDuplicateKey(true)); assertEquals("duplicate key should be overwritten", "value2", jsonObject.getString("key")); } + @Test + public void givenInvalidInputArrays_testStrictModeTrue_shouldThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + List strictModeInputTestCases = getNonCompliantJSONList(); + + strictModeInputTestCases.forEach( + testCase -> assertThrows("expected non-compliant array but got instead: " + testCase, JSONException.class, + () -> new JSONArray(testCase, jsonParserConfiguration))); + } + + @Test + public void givenCompliantJSONArrayFile_testStrictModeTrue_shouldNotThrowAnyException() throws IOException { + try (Stream lines = Files.lines(Paths.get("src/test/resources/compliantJsonArray.json"))) { + String compliantJsonArrayAsString = lines.collect(Collectors.joining()); + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + new JSONArray(compliantJsonArrayAsString, jsonParserConfiguration); + } + + } + + @Test + public void givenInvalidInputArrays_testStrictModeFalse_shouldNotThrowAnyException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + + List strictModeInputTestCases = getNonCompliantJSONList(); + + strictModeInputTestCases.forEach(testCase -> new JSONArray(testCase, jsonParserConfiguration)); + } + + @Test + public void givenInvalidInputArray_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[1,2];[3,4]"; + + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); + + assertEquals("invalid character found after end of array: ; at 6 [character 7 line 1]", je.getMessage()); + } + + @Test + public void givenInvalidInputArrayWithNumericStrings_testStrictModeTrue_shouldThrowInvalidCharacterErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[\"1\",\"2\"];[3,4]"; + + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); + + assertEquals("invalid character found after end of array: ; at 10 [character 11 line 1]", je.getMessage()); + } + + @Test + public void givenInvalidInputArray_testStrictModeTrue_shouldThrowValueNotSurroundedByQuotesErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[{\"test\": implied}]"; + + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); + + assertEquals("Value is not surrounded by quotes: implied", je.getMessage()); + } + + @Test + public void givenInvalidInputArray_testStrictModeFalse_shouldNotThrowAnyException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + + String testCase = "[{\"test\": implied}]"; + + new JSONArray(testCase, jsonParserConfiguration); + } + + @Test + public void givenNonCompliantQuotes_testStrictModeTrue_shouldThrowJsonExceptionWithConcreteErrorDescription() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCaseOne = "[\"abc', \"test\"]"; + String testCaseTwo = "['abc\", \"test\"]"; + String testCaseThree = "['abc']"; + String testCaseFour = "[{'testField': \"testValue\"}]"; + + JSONException jeOne = assertThrows(JSONException.class, + () -> new JSONArray(testCaseOne, jsonParserConfiguration)); + JSONException jeTwo = assertThrows(JSONException.class, + () -> new JSONArray(testCaseTwo, jsonParserConfiguration)); + JSONException jeThree = assertThrows(JSONException.class, + () -> new JSONArray(testCaseThree, jsonParserConfiguration)); + JSONException jeFour = assertThrows(JSONException.class, + () -> new JSONArray(testCaseFour, jsonParserConfiguration)); + + assertEquals( + "Field contains unbalanced quotes. Starts with \" but ends with single quote. at 6 [character 7 line 1]", + jeOne.getMessage()); + assertEquals( + "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + jeTwo.getMessage()); + assertEquals( + "Single quote wrap not allowed in strict mode at 2 [character 3 line 1]", + jeThree.getMessage()); + assertEquals( + "Single quote wrap not allowed in strict mode at 3 [character 4 line 1]", + jeFour.getMessage()); + } + + @Test + public void givenUnbalancedQuotes_testStrictModeFalse_shouldThrowJsonException() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(false); + + String testCaseOne = "[\"abc', \"test\"]"; + String testCaseTwo = "['abc\", \"test\"]"; + + JSONException jeOne = assertThrows(JSONException.class, + () -> new JSONArray(testCaseOne, jsonParserConfiguration)); + JSONException jeTwo = assertThrows(JSONException.class, + () -> new JSONArray(testCaseTwo, jsonParserConfiguration)); + + assertEquals("Expected a ',' or ']' at 10 [character 11 line 1]", jeOne.getMessage()); + assertEquals("Unterminated string. Character with int code 0 is not allowed within a quoted string. at 15 [character 16 line 1]", jeTwo.getMessage()); + } + + + @Test + public void givenInvalidInputArray_testStrictModeTrue_shouldThrowKeyNotSurroundedByQuotesErrorMessage() { + JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() + .withStrictMode(true); + + String testCase = "[{test: implied}]"; + JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase, + JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration)); + + assertEquals(String.format("Value is not surrounded by quotes: %s", "test"), je.getMessage()); + } + @Test public void verifyDuplicateKeyThenMaxDepth() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() - .withOverwriteDuplicateKey(true) - .withMaxNestingDepth(42); + .withOverwriteDuplicateKey(true) + .withMaxNestingDepth(42); assertEquals(42, jsonParserConfiguration.getMaxNestingDepth()); assertTrue(jsonParserConfiguration.isOverwriteDuplicateKey()); @@ -37,10 +193,28 @@ public void verifyDuplicateKeyThenMaxDepth() { @Test public void verifyMaxDepthThenDuplicateKey() { JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration() - .withMaxNestingDepth(42) - .withOverwriteDuplicateKey(true); + .withMaxNestingDepth(42) + .withOverwriteDuplicateKey(true); assertTrue(jsonParserConfiguration.isOverwriteDuplicateKey()); assertEquals(42, jsonParserConfiguration.getMaxNestingDepth()); } + + /** + * This method contains short but focused use-case samples and is exclusively used to test strictMode unit tests in + * this class. + * + * @return List with JSON strings. + */ + private List getNonCompliantJSONList() { + return Arrays.asList( + "[1,2];[3,4]", + "[test]", + "[{'testSingleQuote': 'testSingleQuote'}]", + "[1, 2,3]:[4,5]", + "[{test: implied}]", + "[{\"test\": implied}]", + "[{\"number\":\"7990154836330\",\"color\":'c'},{\"number\":8784148854580,\"color\":RosyBrown},{\"number\":\"5875770107113\",\"color\":\"DarkSeaGreen\"}]", + "[{test: \"implied\"}]"); + } } diff --git a/src/test/resources/compliantJsonArray.json b/src/test/resources/compliantJsonArray.json new file mode 100644 index 000000000..c37369027 --- /dev/null +++ b/src/test/resources/compliantJsonArray.json @@ -0,0 +1,317 @@ +[ + { + "_id": "6606c27d2ab4a0102d49420a", + "index": 0, + "guid": "441331fb-84d1-4873-a649-3814621a0370", + "isActive": true, + "balance": "$2,691.63", + "picture": "http://example.abc/32x32", + "age": 26, + "eyeColor": "blue", + "name": "abc", + "gender": "female", + "company": "example", + "email": "abc@def.com", + "phone": "+1 (123) 456-7890", + "address": "123 Main St", + "about": "Laborum magna tempor officia irure cillum nulla incididunt Lorem dolor veniam elit cupidatat amet. Veniam veniam exercitation nulla consectetur officia esse ex sunt nulla nisi ea cillum nisi reprehenderit. Qui aliquip reprehenderit aliqua aliquip aliquip anim sit magna nostrud dolore veniam velit elit aliquip.\r\n", + "registered": "2016-07-22T03:18:11 -01:00", + "latitude": -21.544934, + "longitude": 72.765495, + "tags": [ + "consectetur", + "minim", + "sunt", + "in", + "ut", + "velit", + "anim" + ], + "friends": [ + { + "id": 0, + "name": "abc def" + }, + { + "id": 1, + "name": "ghi jkl" + }, + { + "id": 2, + "name": "mno pqr" + } + ], + "greeting": "Hello, abc! You have 10 unread messages.", + "favoriteFruit": "banana" + }, + { + "_id": "6606c27d0a45df5121fb765f", + "index": 1, + "guid": "fd774715-de85-44b9-b498-c214d8f68d9f", + "isActive": true, + "balance": "$2,713.96", + "picture": "http://placehold.it/32x32", + "age": 27, + "eyeColor": "green", + "name": "def", + "gender": "female", + "company": "sample", + "email": "def@abc.com", + "phone": "+1 (123) 456-78910", + "address": "1234 Main St", + "about": "Ea id cupidatat eiusmod culpa. Nulla consequat esse elit enim et pariatur eiusmod ipsum. Consequat eu non reprehenderit in.\r\n", + "registered": "2015-04-06T07:54:22 -01:00", + "latitude": 83.512347, + "longitude": -9.368739, + "tags": [ + "excepteur", + "non", + "nostrud", + "laboris", + "laboris", + "qui", + "aute" + ], + "friends": [ + { + "id": 0, + "name": "sample example" + }, + { + "id": 1, + "name": "test name" + }, + { + "id": 2, + "name": "aaa aaaa" + } + ], + "greeting": "Hello, test! You have 7 unread messages.", + "favoriteFruit": "apple" + }, + { + "_id": "6606c27dfb3a0e4e7e7183d3", + "index": 2, + "guid": "688b0c36-98e0-4ee7-86b8-863638d79b5f", + "isActive": false, + "balance": "$3,514.35", + "picture": "http://placehold.it/32x32", + "age": 32, + "eyeColor": "green", + "name": "test", + "gender": "female", + "company": "test", + "email": "test@test.com", + "phone": "+1 (123) 456-7890", + "address": "123 Main St", + "about": "Mollit officia adipisicing ex nisi non Lorem sunt quis est. Irure exercitation duis ipsum qui ullamco eu ea commodo occaecat minim proident. Incididunt nostrud ex cupidatat eiusmod mollit anim irure culpa. Labore voluptate voluptate labore nisi sit eu. Dolor sit proident velit dolor deserunt labore sit ipsum incididunt eiusmod reprehenderit voluptate. Duis anim velit officia laboris consequat officia dolor sint dolor nisi ex.\r\n", + "registered": "2021-11-02T12:50:05 -00:00", + "latitude": -82.969939, + "longitude": 86.415645, + "tags": [ + "aliquip", + "et", + "est", + "nulla", + "nulla", + "tempor", + "adipisicing" + ], + "friends": [ + { + "id": 0, + "name": "test" + }, + { + "id": 1, + "name": "sample" + }, + { + "id": 2, + "name": "example" + } + ], + "greeting": "Hello, test! You have 1 unread messages.", + "favoriteFruit": "strawberry" + }, + { + "_id": "6606c27d204bc2327fc9ba23", + "index": 3, + "guid": "be970cba-306e-4cbd-be08-c265a43a61fa", + "isActive": true, + "balance": "$3,691.63", + "picture": "http://placehold.it/32x32", + "age": 35, + "eyeColor": "brown", + "name": "another test", + "gender": "male", + "company": "TEST", + "email": "anothertest@anothertest.com", + "phone": "+1 (321) 987-6543", + "address": "123 Example Main St", + "about": "Do proident consectetur minim quis. In adipisicing culpa Lorem fugiat cillum exercitation velit velit. Non voluptate laboris deserunt veniam et sint consectetur irure aliqua quis eiusmod consectetur elit id. Ex sint do anim Lorem excepteur eu nulla.\r\n", + "registered": "2020-06-25T04:55:25 -01:00", + "latitude": 63.614955, + "longitude": -109.299405, + "tags": [ + "irure", + "esse", + "non", + "mollit", + "laborum", + "adipisicing", + "ad" + ], + "friends": [ + { + "id": 0, + "name": "test" + }, + { + "id": 1, + "name": "sample" + }, + { + "id": 2, + "name": "example" + } + ], + "greeting": "Hello, another test! You have 5 unread messages.", + "favoriteFruit": "apple" + }, + { + "_id": "6606c27df63eb5f390cb9989", + "index": 4, + "guid": "2c3e5115-758d-468e-99c5-c9afa26e1f9f", + "isActive": true, + "balance": "$1,047.20", + "picture": "http://test.it/32x32", + "age": 30, + "eyeColor": "green", + "name": "Test Name", + "gender": "female", + "company": "test", + "email": "testname@testname.com", + "phone": "+1 (999) 999-9999", + "address": "999 Test Main St", + "about": "Voluptate exercitation tempor consectetur velit magna ea occaecat cupidatat consectetur anim aute. Aliquip est aute ipsum laboris non irure qui consectetur tempor quis do ea Lorem. Cupidatat exercitation ad culpa aliqua amet commodo mollit reprehenderit exercitation adipisicing amet et laborum pariatur.\r\n", + "registered": "2023-01-19T02:43:18 -00:00", + "latitude": 14.15208, + "longitude": 170.411535, + "tags": [ + "dolor", + "qui", + "cupidatat", + "aliqua", + "laboris", + "reprehenderit", + "sint" + ], + "friends": [ + { + "id": 0, + "name": "test" + }, + { + "id": 1, + "name": "sample" + }, + { + "id": 2, + "name": "example" + } + ], + "greeting": "Hello, test! You have 6 unread messages.", + "favoriteFruit": "apple" + }, + { + "_id": "6606c27d01d19fa29853d59c", + "index": 5, + "guid": "816cda74-5d4b-498f-9724-20f340d5f5bf", + "isActive": false, + "balance": "$2,628.74", + "picture": "http://testing.it/32x32", + "age": 28, + "eyeColor": "green", + "name": "Testing", + "gender": "female", + "company": "test", + "email": "testing@testing.com", + "phone": "+1 (888) 888-8888", + "address": "123 Main St", + "about": "Cupidatat non ut nulla qui excepteur in minim non et nulla fugiat. Dolor quis laborum occaecat veniam dolor ullamco deserunt amet veniam dolor quis proident tempor laboris. In cillum duis ut quis. Aliqua cupidatat magna proident velit tempor veniam et consequat laborum ex dolore qui. Incididunt deserunt magna minim Lorem consectetur.\r\n", + "registered": "2017-10-14T11:14:08 -01:00", + "latitude": -5.345728, + "longitude": -9.706491, + "tags": [ + "officia", + "velit", + "laboris", + "qui", + "cupidatat", + "cupidatat", + "ad" + ], + "friends": [ + { + "id": 0, + "name": "test" + }, + { + "id": 1, + "name": "sample" + }, + { + "id": 2, + "name": "example" + } + ], + "greeting": "Hello, testing! You have 2 unread messages.", + "favoriteFruit": "strawberry" + }, + { + "_id": "6606c27d803003cede1d6deb", + "index": 6, + "guid": "4ee550bc-0920-4104-b3ce-ebf9db6a803f", + "isActive": true, + "balance": "$1,709.31", + "picture": "http://sample.it/32x32", + "age": 31, + "eyeColor": "blue", + "name": "Sample Name", + "gender": "female", + "company": "Sample", + "email": "sample@sample.com", + "phone": "+1 (777) 777-7777", + "address": "123 Main St", + "about": "Lorem ex proident ipsum ullamco velit sit nisi eiusmod cillum. Id tempor irure culpa nisi sit non qui veniam non ut. Aliquip reprehenderit excepteur mollit quis excepteur ex sit. Quis do eu veniam do ullamco occaecat eu cupidatat nisi laborum tempor minim fugiat pariatur. Ex in nulla ex velit.\r\n", + "registered": "2019-04-08T03:54:36 -01:00", + "latitude": -70.660321, + "longitude": 71.547525, + "tags": [ + "consequat", + "veniam", + "pariatur", + "aliqua", + "cillum", + "eu", + "officia" + ], + "friends": [ + { + "id": 0, + "name": "Test" + }, + { + "id": 1, + "name": "Sample" + }, + { + "id": 2, + "name": "Example" + } + ], + "greeting": "Hello, Sample! You have 6 unread messages.", + "favoriteFruit": "apple" + } +] \ No newline at end of file