From bcbbc89568439d0b211482c6fd9d6fa12140d096 Mon Sep 17 00:00:00 2001 From: XIAYM-gh Date: Wed, 2 Sep 2026 20:31:40 +0800 Subject: [PATCH 1/2] Fixes corner case where strict mode isn't working when JSONTokener's next() and back() are called first --- src/main/java/org/json/JSONArray.java | 18 +++++++++++++--- src/main/java/org/json/JSONObject.java | 21 +++++++++++++++++-- src/main/java/org/json/JSONTokener.java | 13 ++++++++++-- .../java/org/json/junit/JSONArrayTest.java | 18 ++++++++++++++++ .../java/org/json/junit/JSONObjectTest.java | 18 ++++++++++++++++ 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index 0d7fde9df..62eca71fe 100644 --- a/src/main/java/org/json/JSONArray.java +++ b/src/main/java/org/json/JSONArray.java @@ -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 eofRequired 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 eofRequired) throws JSONException { this(); - boolean isInitial = x.getPrevious() == 0; if (x.nextClean() != '[') { throw x.syntaxError("A JSONArray text must start with '['"); } @@ -118,10 +130,10 @@ public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) x.back(); this.myArrayList.add(x.nextValue()); } - if (checkForSyntaxError(x, jsonParserConfiguration, isInitial)) return; + if (checkForSyntaxError(x, jsonParserConfiguration, eofRequired)) return; } } else { - if (isInitial && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) { + if (eofRequired && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) { throw x.syntaxError("Strict mode error: Unparsed characters found at end of input text"); } } diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index bcd218e5d..fcce72ab7 100644 --- a/src/main/java/org/json/JSONObject.java +++ b/src/main/java/org/json/JSONObject.java @@ -214,14 +214,31 @@ public JSONObject(JSONTokener x) throws JSONException { * 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 eofRequired + * 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 eofRequired) throws JSONException { this(); - boolean isInitial = x.getPrevious() == 0; if (x.nextClean() != '{') { throw x.syntaxError("A JSONObject text must begin with '{'"); } for (;;) { - if (parseJSONObject(x, jsonParserConfiguration, isInitial)) { + if (parseJSONObject(x, jsonParserConfiguration, eofRequired)) { return; } } diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index 3726856d3..e8ae9fcf1 100644 --- a/src/main/java/org/json/JSONTokener.java +++ b/src/main/java/org/json/JSONTokener.java @@ -120,6 +120,15 @@ public void setJsonParserConfiguration(JSONParserConfiguration jsonParserConfigu this.jsonParserConfiguration = jsonParserConfiguration; } + /** + * Returns whether the tokener is currently positioned at the beginning, + * + * @return true if the current input position is the beginning + */ + public boolean isAtStart() { + return this.index == 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 @@ -458,14 +467,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); } diff --git a/src/test/java/org/json/junit/JSONArrayTest.java b/src/test/java/org/json/junit/JSONArrayTest.java index c54b61795..ad22d9615 100644 --- a/src/test/java/org/json/junit/JSONArrayTest.java +++ b/src/test/java/org/json/junit/JSONArrayTest.java @@ -1566,4 +1566,22 @@ 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")); + } } diff --git a/src/test/java/org/json/junit/JSONObjectTest.java b/src/test/java/org/json/junit/JSONObjectTest.java index 6b692789e..8c40fc7c3 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -4337,4 +4337,22 @@ 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")); + } + } From 359b43d8dfed7395d155bdd7b69fe9bafbac7fd1 Mon Sep 17 00:00:00 2001 From: XIAYM-gh Date: Fri, 4 Sep 2026 08:09:14 +0800 Subject: [PATCH 2/2] Apply corner-case fixes under instruction --- src/main/java/org/json/JSONArray.java | 10 +++--- src/main/java/org/json/JSONObject.java | 18 +++++----- src/main/java/org/json/JSONTokener.java | 21 +++++++++--- .../java/org/json/junit/JSONArrayTest.java | 34 +++++++++++++++++++ .../java/org/json/junit/JSONObjectTest.java | 33 ++++++++++++++++++ 5 files changed, 98 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/json/JSONArray.java b/src/main/java/org/json/JSONArray.java index 62eca71fe..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()); @@ -103,10 +103,10 @@ public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) * * @param x A JSONTokener instance from which the JSONArray is constructed. * @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser. - * @param eofRequired A boolean that determines whether this array is the root. + * @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 eofRequired) throws JSONException { + JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) throws JSONException { this(); if (x.nextClean() != '[') { @@ -130,10 +130,10 @@ public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) x.back(); this.myArrayList.add(x.nextValue()); } - if (checkForSyntaxError(x, jsonParserConfiguration, eofRequired)) return; + if (checkForSyntaxError(x, jsonParserConfiguration, isInitial)) return; } } else { - if (eofRequired && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) { + if (isInitial && jsonParserConfiguration.isStrictMode() && x.nextClean() != 0) { throw x.syntaxError("Strict mode error: Unparsed characters found at end of input text"); } } diff --git a/src/main/java/org/json/JSONObject.java b/src/main/java/org/json/JSONObject.java index fcce72ab7..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,8 +210,8 @@ 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()); @@ -225,20 +225,20 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration * A JSONTokener object containing the source string. * @param jsonParserConfiguration * Variable to pass parser custom configuration for json parsing. - * @param eofRequired + * @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. + * If there is a syntax error in the source string or a + * duplicated key. */ - JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean eofRequired) throws JSONException { + JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration, boolean isInitial) throws JSONException { this(); if (x.nextClean() != '{') { throw x.syntaxError("A JSONObject text must begin with '{'"); } for (;;) { - if (parseJSONObject(x, jsonParserConfiguration, eofRequired)) { + if (parseJSONObject(x, jsonParserConfiguration, isInitial)) { return; } } diff --git a/src/main/java/org/json/JSONTokener.java b/src/main/java/org/json/JSONTokener.java index e8ae9fcf1..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; @@ -121,12 +124,14 @@ public void setJsonParserConfiguration(JSONParserConfiguration jsonParserConfigu } /** - * Returns whether the tokener is currently positioned at the beginning, + * 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 the current input position is the beginning + * @return true if no non-whitespace character has been read */ - public boolean isAtStart() { - return this.index == 0; + protected boolean isAtStart() { + return this.contentCharCount == 0; } /** @@ -140,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; @@ -240,6 +248,9 @@ public char next() throws JSONException { return 0; } this.incrementIndexes(c); + if (c > ' ') { + this.contentCharCount++; + } this.previous = (char) c; return this.previous; } @@ -558,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(); @@ -569,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 ad22d9615..e59b2e19d 100644 --- a/src/test/java/org/json/junit/JSONArrayTest.java +++ b/src/test/java/org/json/junit/JSONArrayTest.java @@ -1573,7 +1573,24 @@ public void strictModeShouldCheckTrailingCharactersAfterNextAndBack() { 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(); @@ -1584,4 +1601,21 @@ public void strictModeShouldCheckTrailingCharactersAfterNextAndBack() { 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 8c40fc7c3..a0f0638b0 100644 --- a/src/test/java/org/json/junit/JSONObjectTest.java +++ b/src/test/java/org/json/junit/JSONObjectTest.java @@ -4343,7 +4343,24 @@ public void strictModeShouldCheckTrailingCharactersAfterNextAndBack() { 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(); @@ -4355,4 +4372,20 @@ public void strictModeShouldCheckTrailingCharactersAfterNextAndBack() { "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")); + } }