diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java
index 0d7fde9df..d5fb170e9 100644
--- a/src/main/java/org/json/JSONArray.java
+++ b/src/main/java/org/json/JSONArray.java
@@ -80,7 +80,7 @@ public JSONArray() {
* @param x
* A JSONTokener
* @throws JSONException
- * If there is a syntax error.
+ * If there is a syntax error.
*/
public JSONArray(JSONTokener x) throws JSONException {
this(x, x.getJsonParserConfiguration());
@@ -94,9 +94,21 @@ public JSONArray(JSONTokener x) throws JSONException {
* @throws JSONException If a syntax error occurs during the construction of the JSONArray.
*/
public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
+ this(x, jsonParserConfiguration, x.isAtStart());
+ }
+
+ /**
+ * Constructs a JSONArray from a JSONTokener and a JSONParserConfiguration, for internal use.
+ * Never call this instead of using withStrictMode(boolean).
+ *
+ * @param x A JSONTokener instance from which the JSONArray is constructed.
+ * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser.
+ * @param isInitial A boolean that determines whether this array is the root.
+ * @throws JSONException If a syntax error occurs during the construction of the JSONArray.
+ */
+ JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) throws JSONException {
this();
- boolean isInitial = x.getPrevious() == 0;
if (x.nextClean() != '[') {
throw x.syntaxError("A JSONArray text must start with '['");
}
diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java
index bcd218e5d..295dea4b6 100644
--- a/src/main/java/org/json/JSONObject.java
+++ b/src/main/java/org/json/JSONObject.java
@@ -195,8 +195,8 @@ public JSONObject(JSONObject jo, String ... names) {
* @param x
* A JSONTokener object containing the source string.
* @throws JSONException
- * If there is a syntax error in the source string or a
- * duplicated key.
+ * If there is a syntax error in the source string or a
+ * duplicated key.
*/
public JSONObject(JSONTokener x) throws JSONException {
this(x, x.getJsonParserConfiguration());
@@ -210,12 +210,29 @@ public JSONObject(JSONTokener x) throws JSONException {
* @param jsonParserConfiguration
* Variable to pass parser custom configuration for json parsing.
* @throws JSONException
- * If there is a syntax error in the source string or a
- * duplicated key.
+ * If there is a syntax error in the source string or a
+ * duplicated key.
*/
public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
+ this(x, jsonParserConfiguration, x.isAtStart());
+ }
+
+ /**
+ * Construct a JSONObject from a JSONTokener with custom json parse configurations, for internal use.
+ * Never call this instead of using withStrictMode(boolean).
+ *
+ * @param x
+ * A JSONTokener object containing the source string.
+ * @param jsonParserConfiguration
+ * Variable to pass parser custom configuration for json parsing.
+ * @param isInitial
+ * A boolean that determines whether this object is the root.
+ * @throws JSONException
+ * If there is a syntax error in the source string or a
+ * duplicated key.
+ */
+ JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) throws JSONException {
this();
- boolean isInitial = x.getPrevious() == 0;
if (x.nextClean() != '{') {
throw x.syntaxError("A JSONObject text must begin with '{'");
diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java
index 3726856d3..2eaeedee8 100644
--- a/src/main/java/org/json/JSONTokener.java
+++ b/src/main/java/org/json/JSONTokener.java
@@ -31,6 +31,8 @@ public class JSONTokener {
private boolean usePrevious;
/** the number of characters read in the previous line. */
private long characterPreviousLine;
+ /** number of non-whitespace characters read from the source. */
+ private long contentCharCount;
// access to this object is required for strict mode checking
private JSONParserConfiguration jsonParserConfiguration;
@@ -60,6 +62,7 @@ public JSONTokener(Reader reader, JSONParserConfiguration jsonParserConfiguratio
this.usePrevious = false;
this.previous = 0;
this.index = 0;
+ this.contentCharCount = 0;
this.character = 1;
this.characterPreviousLine = 0;
this.line = 1;
@@ -120,6 +123,17 @@ public void setJsonParserConfiguration(JSONParserConfiguration jsonParserConfigu
this.jsonParserConfiguration = jsonParserConfiguration;
}
+ /**
+ * Returns whether the tokener is positioned at the beginning,
+ * i.e. only whitespace characters (or no characters at all) have been read so far.
+ * Consuming and backing up over the first characters does not change the result.
+ *
+ * @return true if no non-whitespace character has been read
+ */
+ protected boolean isAtStart() {
+ return this.contentCharCount == 0;
+ }
+
/**
* Back up one character. This provides a sort of lookahead capability,
* so that you can test for a digit or letter before attempting to parse
@@ -131,6 +145,9 @@ public void back() throws JSONException {
if (this.usePrevious || this.index <= 0) {
throw new JSONException("Stepping back two steps is not supported");
}
+ if (this.previous > ' ') {
+ this.contentCharCount--;
+ }
this.decrementIndexes();
this.usePrevious = true;
this.eof = false;
@@ -231,6 +248,9 @@ public char next() throws JSONException {
return 0;
}
this.incrementIndexes(c);
+ if (c > ' ') {
+ this.contentCharCount++;
+ }
this.previous = (char) c;
return this.previous;
}
@@ -458,14 +478,14 @@ public Object nextValue() throws JSONException {
case '{':
this.back();
try {
- return new JSONObject(this, jsonParserConfiguration);
+ return new JSONObject(this, jsonParserConfiguration, false);
} catch (StackOverflowError e) {
throw new JSONException("JSON Array or Object depth too large to process.", e);
}
case '[':
this.back();
try {
- return new JSONArray(this, jsonParserConfiguration);
+ return new JSONArray(this, jsonParserConfiguration, false);
} catch (StackOverflowError e) {
throw new JSONException("JSON Array or Object depth too large to process.", e);
}
@@ -549,6 +569,7 @@ public char skipTo(char to) throws JSONException {
long startIndex = this.index;
long startCharacter = this.character;
long startLine = this.line;
+ long startContentCharCount = this.contentCharCount;
this.reader.mark(1000000);
do {
c = this.next();
@@ -560,6 +581,7 @@ public char skipTo(char to) throws JSONException {
this.index = startIndex;
this.character = startCharacter;
this.line = startLine;
+ this.contentCharCount = startContentCharCount;
return 0;
}
} while (c != to);
diff --git a/src/test/java/org/json/junit/JSONArrayTest.java b/src/test/java/org/json/junit/JSONArrayTest.java
index c54b61795..e59b2e19d 100644
--- a/src/test/java/org/json/junit/JSONArrayTest.java
+++ b/src/test/java/org/json/junit/JSONArrayTest.java
@@ -1566,4 +1566,56 @@ public void TestLenientCommas() {
"[1,null,3]", jsonArray.toString());
}
}
+
+ @Test
+ public void strictModeShouldCheckTrailingCharactersAfterNextAndBack() {
+ JSONParserConfiguration strict =
+ new JSONParserConfiguration().withStrictMode();
+
+ JSONTokener tok = new JSONTokener("[]xxx");
+ tok.next();
+ tok.back();
+
+ JSONException exception = assertThrows(
+ JSONException.class,
+ () -> new JSONArray(tok, strict));
+
+ assertTrue(exception.getMessage().contains(
+ "Unparsed characters found at end of input text"));
+ }
+
+ @Test
+ public void strictModeShouldCheckTrailingCharactersAfterConsumedWhitespace() {
+ JSONParserConfiguration strict =
+ new JSONParserConfiguration().withStrictMode();
+
+ JSONTokener tok = new JSONTokener(" []xxx");
+ tok.next();
+ tok.next();
+ tok.back();
+
+ JSONException exception = assertThrows(
+ JSONException.class,
+ () -> new JSONArray(tok, strict));
+
+ assertTrue(exception.getMessage().contains(
+ "Unparsed characters found at end of input text"));
+ }
+
+ @Test
+ public void strictModeShouldCheckTrailingCharactersAfterNextCleanAndBack() {
+ JSONParserConfiguration strict =
+ new JSONParserConfiguration().withStrictMode();
+
+ JSONTokener tok = new JSONTokener(" []xxx");
+ tok.nextClean();
+ tok.back();
+
+ JSONException exception = assertThrows(
+ JSONException.class,
+ () -> new JSONArray(tok, strict));
+
+ assertTrue(exception.getMessage().contains(
+ "Unparsed characters found at end of input text"));
+ }
}
diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java
index 6b692789e..a0f0638b0 100644
--- a/src/test/java/org/json/junit/JSONObjectTest.java
+++ b/src/test/java/org/json/junit/JSONObjectTest.java
@@ -4337,4 +4337,55 @@ public void testStringToNumberInvalidFormats() {
}
}
+ @Test
+ public void strictModeShouldCheckTrailingCharactersAfterNextAndBack() {
+ JSONParserConfiguration strict =
+ new JSONParserConfiguration().withStrictMode();
+
+ JSONTokener tok = new JSONTokener("{}xxx");
+ tok.next();
+ tok.back();
+
+ JSONException exception = assertThrows(
+ JSONException.class,
+ () -> new JSONObject(tok, strict));
+
+ assertTrue(exception.getMessage().contains(
+ "Unparsed characters found at end of input text"));
+ }
+
+ @Test
+ public void strictModeShouldCheckTrailingCharactersAfterConsumedWhitespace() {
+ JSONParserConfiguration strict =
+ new JSONParserConfiguration().withStrictMode();
+
+ JSONTokener tok = new JSONTokener(" {}xxx");
+ tok.next();
+ tok.next();
+ tok.back();
+
+ JSONException exception = assertThrows(
+ JSONException.class,
+ () -> new JSONObject(tok, strict));
+
+ assertTrue(exception.getMessage().contains(
+ "Unparsed characters found at end of input text"));
+ }
+
+ @Test
+ public void strictModeShouldCheckTrailingCharactersAfterNextCleanAndBack() {
+ JSONParserConfiguration strict =
+ new JSONParserConfiguration().withStrictMode();
+
+ JSONTokener tok = new JSONTokener(" {}xxx");
+ tok.nextClean();
+ tok.back();
+
+ JSONException exception = assertThrows(
+ JSONException.class,
+ () -> new JSONObject(tok, strict));
+
+ assertTrue(exception.getMessage().contains(
+ "Unparsed characters found at end of input text"));
+ }
}