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 If If an array has 2 or more elements, then it will be output across
* multiple lines:
* 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 If an array has 2 or more elements, then it will be output across
* multiple lines:
+ * 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 [
(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
@@ -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.
- *
+ *
* {@code indentFactor > 0}
and the {@link JSONArray} has only
* one element, then the array will be output on a single line:
* {@code [1]}
- *
+ *
* {@code
* [
@@ -1683,7 +1714,7 @@ public String toString() {
*
{@code indentFactor > 0}
and the {@link JSONArray} has only
* one element, then the array will be output on a single line:
* {@code [1]}
- *
+ *
* {@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.
+ *
"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