Skip to content

Commit ac40a6f

Browse files
authored
Merge pull request #921 from stleary/restore-jsonparserconfiguration
Restore strict mode text parsing
2 parents 2ee5bf1 + 2dcef89 commit ac40a6f

15 files changed

+5170
-407
lines changed

src/main/java/org/json/JSONArray.java

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ public class JSONArray implements Iterable<Object> {
6767
*/
6868
private final ArrayList<Object> myArrayList;
6969

70+
// strict mode checks after constructor require access to this object
71+
private JSONTokener jsonTokener;
72+
73+
// strict mode checks after constructor require access to this object
74+
private JSONParserConfiguration jsonParserConfiguration;
75+
7076
/**
7177
* Construct an empty JSONArray.
7278
*/
@@ -83,11 +89,31 @@ public JSONArray() {
8389
* If there is a syntax error.
8490
*/
8591
public JSONArray(JSONTokener x) throws JSONException {
92+
this(x, new JSONParserConfiguration());
93+
}
94+
95+
/**
96+
* Constructs a JSONArray from a JSONTokener and a JSONParserConfiguration.
97+
*
98+
* @param x A JSONTokener instance from which the JSONArray is constructed.
99+
* @param jsonParserConfiguration A JSONParserConfiguration instance that controls the behavior of the parser.
100+
* @throws JSONException If a syntax error occurs during the construction of the JSONArray.
101+
*/
102+
public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
86103
this();
104+
105+
if (this.jsonParserConfiguration == null) {
106+
this.jsonParserConfiguration = jsonParserConfiguration;
107+
}
108+
if (this.jsonTokener == null) {
109+
this.jsonTokener = x;
110+
this.jsonTokener.setJsonParserConfiguration(this.jsonParserConfiguration);
111+
}
112+
87113
if (x.nextClean() != '[') {
88114
throw x.syntaxError("A JSONArray text must start with '['");
89115
}
90-
116+
91117
char nextChar = x.nextClean();
92118
if (nextChar == 0) {
93119
// array is unclosed. No ']' found, instead EOF
@@ -114,6 +140,17 @@ public JSONArray(JSONTokener x) throws JSONException {
114140
throw x.syntaxError("Expected a ',' or ']'");
115141
}
116142
if (nextChar == ']') {
143+
// trailing commas are not allowed in strict mode
144+
if (jsonParserConfiguration.isStrictMode()) {
145+
throw x.syntaxError("Strict mode error: Expected another array element");
146+
}
147+
return;
148+
}
149+
if (nextChar == ',') {
150+
// consecutive commas are not allowed in strict mode
151+
if (jsonParserConfiguration.isStrictMode()) {
152+
throw x.syntaxError("Strict mode error: Expected a valid array element");
153+
}
117154
return;
118155
}
119156
x.back();
@@ -138,7 +175,32 @@ public JSONArray(JSONTokener x) throws JSONException {
138175
* If there is a syntax error.
139176
*/
140177
public JSONArray(String source) throws JSONException {
141-
this(new JSONTokener(source));
178+
this(source, new JSONParserConfiguration());
179+
// Strict mode does not allow trailing chars
180+
if (this.jsonParserConfiguration.isStrictMode() &&
181+
this.jsonTokener.nextClean() != 0) {
182+
throw jsonTokener.syntaxError("Strict mode error: Unparsed characters found at end of input text");
183+
}
184+
}
185+
186+
/**
187+
* Construct a JSONArray from a source JSON text.
188+
*
189+
* @param source
190+
* A string that begins with <code>[</code>&nbsp;<small>(left
191+
* bracket)</small> and ends with <code>]</code>
192+
* &nbsp;<small>(right bracket)</small>.
193+
* @param jsonParserConfiguration the parser config object
194+
* @throws JSONException
195+
* If there is a syntax error.
196+
*/
197+
public JSONArray(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
198+
this(new JSONTokener(source), jsonParserConfiguration);
199+
// Strict mode does not allow trailing chars
200+
if (this.jsonParserConfiguration.isStrictMode() &&
201+
this.jsonTokener.nextClean() != 0) {
202+
throw jsonTokener.syntaxError("Strict mode error: Unparsed characters found at end of input text");
203+
}
142204
}
143205

144206
/**

src/main/java/org/json/JSONObject.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ public Class<? extends Map> getMapType() {
152152
*/
153153
public static final Object NULL = new Null();
154154

155+
// strict mode checks after constructor require access to this object
156+
private JSONTokener jsonTokener;
157+
158+
// strict mode checks after constructor require access to this object
159+
private JSONParserConfiguration jsonParserConfiguration;
160+
155161
/**
156162
* Construct an empty JSONObject.
157163
*/
@@ -211,6 +217,15 @@ public JSONObject(JSONTokener x) throws JSONException {
211217
*/
212218
public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
213219
this();
220+
221+
if (this.jsonParserConfiguration == null) {
222+
this.jsonParserConfiguration = jsonParserConfiguration;
223+
}
224+
if (this.jsonTokener == null) {
225+
this.jsonTokener = x;
226+
this.jsonTokener.setJsonParserConfiguration(this.jsonParserConfiguration);
227+
}
228+
214229
char c;
215230
String key;
216231

@@ -255,8 +270,16 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration
255270

256271
switch (x.nextClean()) {
257272
case ';':
273+
// In strict mode semicolon is not a valid separator
274+
if (jsonParserConfiguration.isStrictMode()) {
275+
throw x.syntaxError("Strict mode error: Invalid character ';' found");
276+
}
258277
case ',':
259278
if (x.nextClean() == '}') {
279+
// trailing commas are not allowed in strict mode
280+
if (jsonParserConfiguration.isStrictMode()) {
281+
throw x.syntaxError("Strict mode error: Expected another object element");
282+
}
260283
return;
261284
}
262285
if (x.end()) {
@@ -433,6 +456,11 @@ public JSONObject(Object object, String ... names) {
433456
*/
434457
public JSONObject(String source) throws JSONException {
435458
this(source, new JSONParserConfiguration());
459+
// Strict mode does not allow trailing chars
460+
if (this.jsonParserConfiguration.isStrictMode() &&
461+
this.jsonTokener.nextClean() != 0) {
462+
throw new JSONException("Strict mode error: Unparsed characters found at end of input text");
463+
}
436464
}
437465

438466
/**
@@ -451,6 +479,11 @@ public JSONObject(String source) throws JSONException {
451479
*/
452480
public JSONObject(String source, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
453481
this(new JSONTokener(source), jsonParserConfiguration);
482+
// Strict mode does not allow trailing chars
483+
if (this.jsonParserConfiguration.isStrictMode() &&
484+
this.jsonTokener.nextClean() != 0) {
485+
throw new JSONException("Strict mode error: Unparsed characters found at end of input text");
486+
}
454487
}
455488

456489
/**

src/main/java/org/json/JSONParserConfiguration.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ public JSONParserConfiguration() {
1717
this.overwriteDuplicateKey = false;
1818
}
1919

20+
/**
21+
* This flag, when set to true, instructs the parser to enforce strict mode when parsing JSON text.
22+
* Garbage chars at the end of the doc, unquoted string, and single-quoted strings are all disallowed.
23+
*/
24+
private boolean strictMode;
25+
2026
@Override
2127
protected JSONParserConfiguration clone() {
2228
JSONParserConfiguration clone = new JSONParserConfiguration();
@@ -58,6 +64,35 @@ public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwrite
5864
return clone;
5965
}
6066

67+
/**
68+
* Sets the strict mode configuration for the JSON parser with default true value
69+
* <p>
70+
* When strict mode is enabled, the parser will throw a JSONException if it encounters an invalid character
71+
* immediately following the final ']' character in the input. This is useful for ensuring strict adherence to the
72+
* JSON syntax, as any characters after the final closing bracket of a JSON array are considered invalid.
73+
* @return a new JSONParserConfiguration instance with the updated strict mode setting
74+
*/
75+
public JSONParserConfiguration withStrictMode() {
76+
return withStrictMode(true);
77+
}
78+
79+
/**
80+
* Sets the strict mode configuration for the JSON parser.
81+
* <p>
82+
* When strict mode is enabled, the parser will throw a JSONException if it encounters an invalid character
83+
* immediately following the final ']' character in the input. This is useful for ensuring strict adherence to the
84+
* JSON syntax, as any characters after the final closing bracket of a JSON array are considered invalid.
85+
*
86+
* @param mode a boolean value indicating whether strict mode should be enabled or not
87+
* @return a new JSONParserConfiguration instance with the updated strict mode setting
88+
*/
89+
public JSONParserConfiguration withStrictMode(final boolean mode) {
90+
JSONParserConfiguration clone = this.clone();
91+
clone.strictMode = mode;
92+
93+
return clone;
94+
}
95+
6196
/**
6297
* The parser's behavior when meeting duplicate keys, controls whether the parser should
6398
* overwrite duplicate keys or not.
@@ -67,4 +102,11 @@ public JSONParserConfiguration withOverwriteDuplicateKey(final boolean overwrite
67102
public boolean isOverwriteDuplicateKey() {
68103
return this.overwriteDuplicateKey;
69104
}
105+
106+
/**
107+
* @return the current strict mode setting.
108+
*/
109+
public boolean isStrictMode() {
110+
return this.strictMode;
111+
}
70112
}

src/main/java/org/json/JSONTokener.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public class JSONTokener {
3232
/** the number of characters read in the previous line. */
3333
private long characterPreviousLine;
3434

35+
// access to this object is required for strict mode checking
36+
private JSONParserConfiguration jsonParserConfiguration;
3537

3638
/**
3739
* Construct a JSONTokener from a Reader. The caller must close the Reader.
@@ -70,6 +72,21 @@ public JSONTokener(String s) {
7072
this(new StringReader(s));
7173
}
7274

75+
/**
76+
* Getter
77+
* @return jsonParserConfiguration
78+
*/
79+
public JSONParserConfiguration getJsonParserConfiguration() {
80+
return jsonParserConfiguration;
81+
}
82+
83+
/**
84+
* Setter
85+
* @param jsonParserConfiguration new value for jsonParserConfiguration
86+
*/
87+
public void setJsonParserConfiguration(JSONParserConfiguration jsonParserConfiguration) {
88+
this.jsonParserConfiguration = jsonParserConfiguration;
89+
}
7390

7491
/**
7592
* Back up one character. This provides a sort of lookahead capability,
@@ -409,14 +426,14 @@ public Object nextValue() throws JSONException {
409426
case '{':
410427
this.back();
411428
try {
412-
return new JSONObject(this);
429+
return new JSONObject(this, jsonParserConfiguration);
413430
} catch (StackOverflowError e) {
414431
throw new JSONException("JSON Array or Object depth too large to process.", e);
415432
}
416433
case '[':
417434
this.back();
418435
try {
419-
return new JSONArray(this);
436+
return new JSONArray(this, jsonParserConfiguration);
420437
} catch (StackOverflowError e) {
421438
throw new JSONException("JSON Array or Object depth too large to process.", e);
422439
}
@@ -427,6 +444,12 @@ public Object nextValue() throws JSONException {
427444
Object nextSimpleValue(char c) {
428445
String string;
429446

447+
// Strict mode only allows strings with explicit double quotes
448+
if (jsonParserConfiguration != null &&
449+
jsonParserConfiguration.isStrictMode() &&
450+
c == '\'') {
451+
throw this.syntaxError("Strict mode error: Single quoted strings are not allowed");
452+
}
430453
switch (c) {
431454
case '"':
432455
case '\'':
@@ -455,7 +478,14 @@ Object nextSimpleValue(char c) {
455478
if ("".equals(string)) {
456479
throw this.syntaxError("Missing value");
457480
}
458-
return JSONObject.stringToValue(string);
481+
Object obj = JSONObject.stringToValue(string);
482+
// Strict mode only allows strings with explicit double quotes
483+
if (jsonParserConfiguration != null &&
484+
jsonParserConfiguration.isStrictMode() &&
485+
obj instanceof String) {
486+
throw this.syntaxError(String.format("Strict mode error: Value '%s' is not surrounded by quotes", obj));
487+
}
488+
return obj;
459489
}
460490

461491

src/test/java/org/json/junit/CDLTest.java

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class CDLTest {
2929
"1, 2, 3, 4\t, 5, 6, 7\n" +
3030
"true, false, true, true, false, false, false\n" +
3131
"0.23, 57.42, 5e27, -234.879, 2.34e5, 0.0, 9e-3\n" +
32-
"\"va\tl1\", \"v\bal2\", \"val3\", \"val\f4\", \"val5\", va'l6, val7\n";
32+
"\"va\tl1\", \"v\bal2\", \"val3\", \"val\f4\", \"val5\", \"va'l6\", val7\n";
3333

3434

3535
/**
@@ -38,11 +38,54 @@ public class CDLTest {
3838
* values all must be quoted in the cases where the JSONObject parsing
3939
* might normally convert the value into a non-string.
4040
*/
41-
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}, " +
42-
"{Col 1:\"1\", Col 2:\"2\", Col 3:\"3\", Col 4:\"4\", Col 5:\"5\", Col 6:\"6\", Col 7:\"7\"}, " +
43-
"{Col 1:\"true\", Col 2:\"false\", Col 3:\"true\", Col 4:\"true\", Col 5:\"false\", Col 6:\"false\", Col 7:\"false\"}, " +
44-
"{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\"}, " +
45-
"{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}]";
41+
private static final String EXPECTED_LINES =
42+
"[ " +
43+
"{" +
44+
"\"Col 1\":\"val1\", " +
45+
"\"Col 2\":\"val2\", " +
46+
"\"Col 3\":\"val3\", " +
47+
"\"Col 4\":\"val4\", " +
48+
"\"Col 5\":\"val5\", " +
49+
"\"Col 6\":\"val6\", " +
50+
"\"Col 7\":\"val7\"" +
51+
"}, " +
52+
" {" +
53+
"\"Col 1\":\"1\", " +
54+
"\"Col 2\":\"2\", " +
55+
"\"Col 3\":\"3\", " +
56+
"\"Col 4\":\"4\", " +
57+
"\"Col 5\":\"5\", " +
58+
"\"Col 6\":\"6\", " +
59+
"\"Col 7\":\"7\"" +
60+
"}, " +
61+
" {" +
62+
"\"Col 1\":\"true\", " +
63+
"\"Col 2\":\"false\", " +
64+
"\"Col 3\":\"true\", " +
65+
"\"Col 4\":\"true\", " +
66+
"\"Col 5\":\"false\", " +
67+
"\"Col 6\":\"false\", " +
68+
"\"Col 7\":\"false\"" +
69+
"}, " +
70+
"{" +
71+
"\"Col 1\":\"0.23\", " +
72+
"\"Col 2\":\"57.42\", " +
73+
"\"Col 3\":\"5e27\", " +
74+
"\"Col 4\":\"-234.879\", " +
75+
"\"Col 5\":\"2.34e5\", " +
76+
"\"Col 6\":\"0.0\", " +
77+
"\"Col 7\":\"9e-3\"" +
78+
"}, " +
79+
"{" +
80+
"\"Col 1\":\"va\tl1\", " +
81+
"\"Col 2\":\"v\bal2\", " +
82+
"\"Col 3\":\"val3\", " +
83+
"\"Col 4\":\"val\f4\", " +
84+
"\"Col 5\":\"val5\", " +
85+
"\"Col 6\":\"va'l6\", " +
86+
"\"Col 7\":\"val7\"" +
87+
"}" +
88+
"]";
4689

4790
/**
4891
* Attempts to create a JSONArray from a null string.
@@ -283,11 +326,11 @@ public void textToJSONArrayPipeDelimited() {
283326
*/
284327
@Test
285328
public void jsonArrayToJSONArray() {
286-
String nameArrayStr = "[Col1, Col2]";
329+
String nameArrayStr = "[\"Col1\", \"Col2\"]";
287330
String values = "V1, V2";
288331
JSONArray nameJSONArray = new JSONArray(nameArrayStr);
289332
JSONArray jsonArray = CDL.toJSONArray(nameJSONArray, values);
290-
JSONArray expectedJsonArray = new JSONArray("[{Col1:V1,Col2:V2}]");
333+
JSONArray expectedJsonArray = new JSONArray("[{\"Col1\":\"V1\",\"Col2\":\"V2\"}]");
291334
Util.compareActualVsExpectedJsonArrays(jsonArray, expectedJsonArray);
292335
}
293336

0 commit comments

Comments
 (0)