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")); + } }