Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Restore strict mode text parsing #921

Merged
merged 6 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions src/main/java/org/json/JSONArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ public class JSONArray implements Iterable<Object> {
*/
private final ArrayList<Object> myArrayList;

private JSONTokener jsonTokener;

private JSONParserConfiguration jsonParserConfiguration;

/**
* Construct an empty JSONArray.
*/
Expand All @@ -83,11 +87,31 @@ public JSONArray() {
* 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.
*
* @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 (this.jsonParserConfiguration == null) {
this.jsonParserConfiguration = jsonParserConfiguration;
}
if (this.jsonTokener == null) {
this.jsonTokener = x;
this.jsonTokener.setJsonParserConfiguration(this.jsonParserConfiguration);
}

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
Expand All @@ -114,6 +138,9 @@ public JSONArray(JSONTokener x) throws JSONException {
throw x.syntaxError("Expected a ',' or ']'");
}
if (nextChar == ']') {
if (jsonParserConfiguration.isStrictMode()) {
throw x.syntaxError("Expected another array element");
}
return;
}
x.back();
Expand All @@ -138,7 +165,36 @@ public JSONArray(JSONTokener x) throws JSONException {
* If there is a syntax error.
*/
public JSONArray(String source) throws JSONException {
this(new JSONTokener(source));
this(source, new JSONParserConfiguration());
if (this.jsonParserConfiguration.isStrictMode()) {
char c = jsonTokener.nextClean();
if (c != 0) {
throw jsonTokener.syntaxError(String.format("invalid character '%s' found after end of array", c));

}
}
}

/**
* Construct a JSONArray from a source JSON text.
*
* @param source
* A string that begins with <code>[</code>&nbsp;<small>(left
* bracket)</small> and ends with <code>]</code>
* &nbsp;<small>(right bracket)</small>.
* @param jsonParserConfiguration the parser config object
* @throws JSONException
* If there is a syntax error.
*/
public JSONArray(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
this(new JSONTokener(source), jsonParserConfiguration);
if (this.jsonParserConfiguration.isStrictMode()) {
char c = jsonTokener.nextClean();
if (c != 0) {
throw jsonTokener.syntaxError(String.format("invalid character '%s' found after end of array", c));

}
}
}

/**
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/org/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ public Class<? extends Map> getMapType() {
*/
public static final Object NULL = new Null();

private JSONTokener jsonTokener;

private JSONParserConfiguration jsonParserConfiguration;

/**
* Construct an empty JSONObject.
*/
Expand Down Expand Up @@ -211,6 +215,15 @@ public JSONObject(JSONTokener x) throws JSONException {
*/
public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
this();

if (this.jsonParserConfiguration == null) {
this.jsonParserConfiguration = jsonParserConfiguration;
}
if (this.jsonTokener == null) {
this.jsonTokener = x;
this.jsonTokener.setJsonParserConfiguration(this.jsonParserConfiguration);
}

char c;
String key;

Expand Down Expand Up @@ -255,8 +268,14 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration

switch (x.nextClean()) {
case ';':
if (jsonParserConfiguration.isStrictMode()) {
throw x.syntaxError("Invalid character ';' found in object in strict mode");
}
case ',':
if (x.nextClean() == '}') {
if (jsonParserConfiguration.isStrictMode()) {
throw x.syntaxError("Expected another object element");
}
return;
}
if (x.end()) {
Expand Down Expand Up @@ -433,6 +452,10 @@ public JSONObject(Object object, String ... names) {
*/
public JSONObject(String source) throws JSONException {
this(source, new JSONParserConfiguration());
if (this.jsonParserConfiguration.isStrictMode() &&
this.jsonTokener.nextClean() != 0) {
throw new JSONException("Unparsed characters found at end of input text");
}
}

/**
Expand All @@ -451,6 +474,13 @@ public JSONObject(String source) throws JSONException {
*/
public JSONObject(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
this(new JSONTokener(source), jsonParserConfiguration);
if (this.jsonParserConfiguration.isStrictMode()) {
char c = jsonTokener.nextClean();
if (c != 0) {
throw jsonTokener.syntaxError(String.format("invalid character '%s' found after end of array", c));

}
}
}

/**
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/org/json/JSONParserConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public JSONParserConfiguration() {
this.overwriteDuplicateKey = false;
}

/**
* This flag, when set to true, instructs the parser to enforce strict mode when parsing JSON text.
* Garbage chars at the end of the doc, unquoted string, and single-quoted strings are all disallowed.
*/
private boolean strictMode;

@Override
protected JSONParserConfiguration clone() {
JSONParserConfiguration clone = new JSONParserConfiguration();
Expand Down Expand Up @@ -58,6 +64,23 @@ public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwrite
return clone;
}

/**
* Sets the strict mode configuration for the JSON parser.
* <p>
* 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.
Expand All @@ -67,4 +90,17 @@ public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwrite
public boolean isOverwriteDuplicateKey() {
return this.overwriteDuplicateKey;
}

/**
* Retrieves the current strict mode setting of the JSON parser.
* <p>
* 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;
}
}
33 changes: 30 additions & 3 deletions src/main/java/org/json/JSONTokener.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class JSONTokener {
/** the number of characters read in the previous line. */
private long characterPreviousLine;

private JSONParserConfiguration jsonParserConfiguration;

/**
* Construct a JSONTokener from a Reader. The caller must close the Reader.
Expand Down Expand Up @@ -70,6 +71,21 @@ public JSONTokener(String s) {
this(new StringReader(s));
}

/**
* Getter
* @return jsonParserConfiguration
*/
public JSONParserConfiguration getJsonParserConfiguration() {
return jsonParserConfiguration;
}

/**
* Setter
* @param jsonParserConfiguration new value for jsonParserConfiguration
*/
public void setJsonParserConfiguration(JSONParserConfiguration jsonParserConfiguration) {
this.jsonParserConfiguration = jsonParserConfiguration;
}

/**
* Back up one character. This provides a sort of lookahead capability,
Expand Down Expand Up @@ -409,14 +425,14 @@ public Object nextValue() throws JSONException {
case '{':
this.back();
try {
return new JSONObject(this);
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);
return new JSONArray(this, jsonParserConfiguration);
} catch (StackOverflowError e) {
throw new JSONException("JSON Array or Object depth too large to process.", e);
}
Expand All @@ -427,6 +443,11 @@ public Object nextValue() throws JSONException {
Object nextSimpleValue(char c) {
String string;

if (jsonParserConfiguration != null &&
jsonParserConfiguration.isStrictMode() &&
c == '\'') {
throw this.syntaxError("Single quote wrap not allowed in strict mode");
}
switch (c) {
case '"':
case '\'':
Expand Down Expand Up @@ -455,7 +476,13 @@ Object nextSimpleValue(char c) {
if ("".equals(string)) {
throw this.syntaxError("Missing value");
}
return JSONObject.stringToValue(string);
Object obj = JSONObject.stringToValue(string);
if (jsonParserConfiguration != null &&
jsonParserConfiguration.isStrictMode() &&
obj instanceof String) {
throw this.syntaxError(String.format("Value '%s' is not surrounded by quotes", obj));
}
return obj;
}


Expand Down
59 changes: 51 additions & 8 deletions src/test/java/org/json/junit/CDLTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class CDLTest {
"1, 2, 3, 4\t, 5, 6, 7\n" +
"true, false, true, true, false, false, false\n" +
"0.23, 57.42, 5e27, -234.879, 2.34e5, 0.0, 9e-3\n" +
"\"va\tl1\", \"v\bal2\", \"val3\", \"val\f4\", \"val5\", va'l6, val7\n";
"\"va\tl1\", \"v\bal2\", \"val3\", \"val\f4\", \"val5\", \"va'l6\", val7\n";


/**
Expand All @@ -38,11 +38,54 @@ public class CDLTest {
* values all must be quoted in the cases where the JSONObject parsing
* might normally convert the value into a non-string.
*/
private static final String EXPECTED_LINES = "[{Col 1:val1, Col 2:val2, Col 3:val3, Col 4:val4, Col 5:val5, Col 6:val6, Col 7:val7}, " +
"{Col 1:\"1\", Col 2:\"2\", Col 3:\"3\", Col 4:\"4\", Col 5:\"5\", Col 6:\"6\", Col 7:\"7\"}, " +
"{Col 1:\"true\", Col 2:\"false\", Col 3:\"true\", Col 4:\"true\", Col 5:\"false\", Col 6:\"false\", Col 7:\"false\"}, " +
"{Col 1:\"0.23\", Col 2:\"57.42\", Col 3:\"5e27\", Col 4:\"-234.879\", Col 5:\"2.34e5\", Col 6:\"0.0\", Col 7:\"9e-3\"}, " +
"{Col 1:\"va\tl1\", Col 2:\"v\bal2\", Col 3:val3, Col 4:\"val\f4\", Col 5:val5, Col 6:va'l6, Col 7:val7}]";
private static final String EXPECTED_LINES =
"[ " +
"{" +
"\"Col 1\":\"val1\", " +
"\"Col 2\":\"val2\", " +
"\"Col 3\":\"val3\", " +
"\"Col 4\":\"val4\", " +
"\"Col 5\":\"val5\", " +
"\"Col 6\":\"val6\", " +
"\"Col 7\":\"val7\"" +
"}, " +
" {" +
"\"Col 1\":\"1\", " +
"\"Col 2\":\"2\", " +
"\"Col 3\":\"3\", " +
"\"Col 4\":\"4\", " +
"\"Col 5\":\"5\", " +
"\"Col 6\":\"6\", " +
"\"Col 7\":\"7\"" +
"}, " +
" {" +
"\"Col 1\":\"true\", " +
"\"Col 2\":\"false\", " +
"\"Col 3\":\"true\", " +
"\"Col 4\":\"true\", " +
"\"Col 5\":\"false\", " +
"\"Col 6\":\"false\", " +
"\"Col 7\":\"false\"" +
"}, " +
"{" +
"\"Col 1\":\"0.23\", " +
"\"Col 2\":\"57.42\", " +
"\"Col 3\":\"5e27\", " +
"\"Col 4\":\"-234.879\", " +
"\"Col 5\":\"2.34e5\", " +
"\"Col 6\":\"0.0\", " +
"\"Col 7\":\"9e-3\"" +
"}, " +
"{" +
"\"Col 1\":\"va\tl1\", " +
"\"Col 2\":\"v\bal2\", " +
"\"Col 3\":\"val3\", " +
"\"Col 4\":\"val\f4\", " +
"\"Col 5\":\"val5\", " +
"\"Col 6\":\"va'l6\", " +
"\"Col 7\":\"val7\"" +
"}" +
"]";

/**
* Attempts to create a JSONArray from a null string.
Expand Down Expand Up @@ -283,11 +326,11 @@ public void textToJSONArrayPipeDelimited() {
*/
@Test
public void jsonArrayToJSONArray() {
String nameArrayStr = "[Col1, Col2]";
String nameArrayStr = "[\"Col1\", \"Col2\"]";
String values = "V1, V2";
JSONArray nameJSONArray = new JSONArray(nameArrayStr);
JSONArray jsonArray = CDL.toJSONArray(nameJSONArray, values);
JSONArray expectedJsonArray = new JSONArray("[{Col1:V1,Col2:V2}]");
JSONArray expectedJsonArray = new JSONArray("[{\"Col1\":\"V1\",\"Col2\":\"V2\"}]");
Util.compareActualVsExpectedJsonArrays(jsonArray, expectedJsonArray);
}

Expand Down
Loading
Loading