Skip to content

Commit 9216a19

Browse files
author
rikkarth
committed
feat(stleary#877): improved JSONArray and JSONTokener logic
JSONArray construction improved to recursive validation JSONTokener implemented smallCharMemory and array level for improved validation Added new test cases and minor test case adaption
1 parent 879579d commit 9216a19

File tree

4 files changed

+151
-79
lines changed

4 files changed

+151
-79
lines changed

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

Lines changed: 56 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -96,87 +96,75 @@ public JSONArray(JSONTokener x) throws JSONException {
9696
*/
9797
public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
9898
this();
99-
if (x.nextClean() != '[') {
99+
char nextChar = x.nextClean();
100+
101+
// check first character, if not '[' throw JSONException
102+
if (nextChar != '[') {
100103
throw x.syntaxError("A JSONArray text must start with '['");
101104
}
102105

103-
char nextChar = x.nextClean();
104-
if (nextChar == 0) {
105-
// array is unclosed. No ']' found, instead EOF
106-
throw x.syntaxError("Expected a ',' or ']'");
107-
}
108-
if (nextChar != ']') {
109-
x.back();
110-
for (;;) {
111-
if (x.nextClean() == ',') {
112-
x.back();
113-
this.myArrayList.add(JSONObject.NULL);
114-
} else {
106+
parseTokener(x, jsonParserConfiguration); // runs recursively
107+
108+
}
109+
110+
private void parseTokener(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) {
111+
boolean strictMode = jsonParserConfiguration.isStrictMode();
112+
113+
char cursor = x.nextClean();
114+
115+
switch (cursor) {
116+
case 0:
117+
throwErrorIfEoF(x);
118+
break;
119+
case ',':
120+
cursor = x.nextClean();
121+
122+
throwErrorIfEoF(x);
123+
124+
if (cursor == ']') {
125+
break;
126+
}
127+
128+
x.back();
129+
130+
parseTokener(x, jsonParserConfiguration);
131+
break;
132+
case ']':
133+
if (strictMode) {
134+
cursor = x.nextClean();
135+
boolean isNotEoF = !x.end();
136+
137+
if (isNotEoF && x.getArrayLevel() == 0) {
138+
throw x.syntaxError(String.format("invalid character '%s' found after end of array", cursor));
139+
}
140+
115141
x.back();
116-
this.myArrayList.add(x.nextValue(jsonParserConfiguration));
117142
}
118-
switch (x.nextClean()) {
119-
case 0:
120-
// array is unclosed. No ']' found, instead EOF
121-
throw x.syntaxError("Expected a ',' or ']'");
122-
case ',':
123-
nextChar = x.nextClean();
124-
if (nextChar == 0) {
125-
// array is unclosed. No ']' found, instead EOF
126-
throw x.syntaxError("Expected a ',' or ']'");
127-
}
128-
if (nextChar == ']') {
129-
return;
130-
}
131-
x.back();
132-
break;
133-
case ']':
134-
if (jsonParserConfiguration.isStrictMode()) {
135-
nextChar = x.nextClean();
136-
137-
if (nextChar == ','){
138-
x.back();
139-
return;
140-
}
141-
142-
if (nextChar == ']'){
143-
x.back();
144-
return;
145-
}
146-
147-
if (nextChar != 0) {
148-
throw x.syntaxError("invalid character found after end of array: " + nextChar);
149-
}
150-
}
151-
152-
return;
153-
default:
154-
throw x.syntaxError("Expected a ',' or ']'");
143+
break;
144+
default:
145+
x.back();
146+
boolean currentCharIsQuote = x.getPrevious() == '"';
147+
boolean quoteIsNotNextToValidChar = x.getPreviousChar() != ',' && x.getPreviousChar() != '[';
148+
149+
if (strictMode && currentCharIsQuote && quoteIsNotNextToValidChar) {
150+
throw x.syntaxError(String.format("invalid character '%s' found after end of array", cursor));
155151
}
156-
}
157-
}
158152

159-
if (jsonParserConfiguration.isStrictMode()) {
160-
validateInput(x);
153+
this.myArrayList.add(x.nextValue(jsonParserConfiguration));
154+
parseTokener(x, jsonParserConfiguration);
161155
}
162156
}
163157

164158
/**
165-
* Checks if Array adheres to strict mode guidelines, if not, throws JSONException providing back the input in the
166-
* error message.
159+
* Throws JSONException if JSONTokener has reached end of file, usually when array is unclosed. No ']' found,
160+
* instead EoF.
167161
*
168-
* @param x tokener used to examine input.
169-
* @throws JSONException if input is not compliant with strict mode guidelines;
162+
* @param x the JSONTokener being evaluated.
163+
* @throws JSONException if JSONTokener has reached end of file.
170164
*/
171-
private void validateInput(JSONTokener x) {
172-
char cursor = x.getPrevious();
173-
174-
boolean isEndOfArray = cursor == ']';
175-
char nextChar = x.nextClean();
176-
boolean nextCharacterIsNotEoF = nextChar != 0;
177-
178-
if (isEndOfArray && nextCharacterIsNotEoF) {
179-
throw x.syntaxError(String.format("Provided Array is not compliant with strict mode guidelines: '%s'", nextChar));
165+
private void throwErrorIfEoF(JSONTokener x) {
166+
if (x.end()) {
167+
throw x.syntaxError(String.format("Expected a ',' or ']' but instead found '%s'", x.getPrevious()));
180168
}
181169
}
182170

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import java.io.*;
44
import java.nio.charset.Charset;
5+
import java.util.ArrayList;
6+
import java.util.List;
57

68
/*
79
Public Domain.
@@ -31,6 +33,8 @@ public class JSONTokener {
3133
private boolean usePrevious;
3234
/** the number of characters read in the previous line. */
3335
private long characterPreviousLine;
36+
private final List<Character> smallCharMemory;
37+
private int arrayLevel = 0;
3438

3539

3640
/**
@@ -49,6 +53,7 @@ public JSONTokener(Reader reader) {
4953
this.character = 1;
5054
this.characterPreviousLine = 0;
5155
this.line = 1;
56+
this.smallCharMemory = new ArrayList<>(2);
5257
}
5358

5459

@@ -186,6 +191,46 @@ public char next() throws JSONException {
186191
return this.previous;
187192
}
188193

194+
private void insertCharacterInCharMemory(Character c) {
195+
boolean foundSameCharRef = checkForEqualCharRefInMicroCharMemory(c);
196+
if(foundSameCharRef){
197+
return;
198+
}
199+
200+
if(smallCharMemory.size() < 2){
201+
smallCharMemory.add(c);
202+
return;
203+
}
204+
205+
smallCharMemory.set(0, smallCharMemory.get(1));
206+
smallCharMemory.remove(1);
207+
smallCharMemory.add(c);
208+
}
209+
210+
private boolean checkForEqualCharRefInMicroCharMemory(Character c) {
211+
boolean isNotEmpty = !smallCharMemory.isEmpty();
212+
if (isNotEmpty) {
213+
Character lastChar = smallCharMemory.get(smallCharMemory.size() - 1);
214+
return c.compareTo(lastChar) == 0;
215+
}
216+
217+
// list is empty so there's no equal characters
218+
return false;
219+
}
220+
221+
/**
222+
* Retrieves the previous char from memory.
223+
*
224+
* @return previous char stored in memory.
225+
*/
226+
public char getPreviousChar() {
227+
return smallCharMemory.get(0);
228+
}
229+
230+
public int getArrayLevel(){
231+
return this.arrayLevel;
232+
}
233+
189234
/**
190235
* Get the last character read from the input or '\0' if nothing has been read yet.
191236
* @return the last character read from the input.
@@ -263,7 +308,6 @@ public String next(int n) throws JSONException {
263308
return new String(chars);
264309
}
265310

266-
267311
/**
268312
* Get the next char in the string, skipping whitespace.
269313
* @throws JSONException Thrown if there is an error reading the source string.
@@ -273,6 +317,7 @@ public char nextClean() throws JSONException {
273317
for (;;) {
274318
char c = this.next();
275319
if (c == 0 || c > ' ') {
320+
insertCharacterInCharMemory(c);
276321
return c;
277322
}
278323
}
@@ -441,6 +486,7 @@ public Object nextValue(JSONParserConfiguration jsonParserConfiguration) throws
441486
case '[':
442487
this.back();
443488
try {
489+
this.arrayLevel++;
444490
return new JSONArray(this, jsonParserConfiguration);
445491
} catch (StackOverflowError e) {
446492
throw new JSONException("JSON Array or Object depth too large to process.", e);
@@ -531,7 +577,7 @@ private Object getValidNumberBooleanOrNullFromObject(Object value) {
531577
return value;
532578
}
533579

534-
throw new JSONException(String.format("Value is not surrounded by quotes: %s", value));
580+
throw this.syntaxError(String.format("Value '%s' is not surrounded by quotes", value));
535581
}
536582

537583
/**

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public void unclosedArray() {
142142
assertNull("Should throw an exception", new JSONArray("["));
143143
} catch (JSONException e) {
144144
assertEquals("Expected an exception message",
145-
"Expected a ',' or ']' at 1 [character 2 line 1]",
145+
"Expected a ',' or ']' but instead found '[' at 1 [character 2 line 1]",
146146
e.getMessage());
147147
}
148148
}
@@ -157,7 +157,7 @@ public void unclosedArray2() {
157157
assertNull("Should throw an exception", new JSONArray("[\"test\""));
158158
} catch (JSONException e) {
159159
assertEquals("Expected an exception message",
160-
"Expected a ',' or ']' at 7 [character 8 line 1]",
160+
"Expected a ',' or ']' but instead found '\"' at 7 [character 8 line 1]",
161161
e.getMessage());
162162
}
163163
}
@@ -172,7 +172,7 @@ public void unclosedArray3() {
172172
assertNull("Should throw an exception", new JSONArray("[\"test\","));
173173
} catch (JSONException e) {
174174
assertEquals("Expected an exception message",
175-
"Expected a ',' or ']' at 8 [character 9 line 1]",
175+
"Expected a ',' or ']' but instead found ',' at 8 [character 9 line 1]",
176176
e.getMessage());
177177
}
178178
}

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

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ public void givenInvalidInputArrays_testStrictModeTrue_shouldThrowJsonException(
4646
() -> new JSONArray(testCase, jsonParserConfiguration)));
4747
}
4848

49+
@Test
50+
public void givenEmptyArray_testStrictModeTrue_shouldNotThrowJsonException(){
51+
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
52+
.withStrictMode(true);
53+
54+
String testCase = "[]";
55+
56+
JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
57+
System.out.println(jsonArray);
58+
}
59+
4960
@Test
5061
public void givenValidDoubleArray_testStrictModeTrue_shouldNotThrowJsonException() {
5162
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
@@ -63,6 +74,30 @@ public void givenValidDoubleArray_testStrictModeTrue_shouldNotThrowJsonException
6374
assertTrue(arrayShouldContainBooleanAt0.get(0) instanceof Boolean);
6475
}
6576

77+
@Test
78+
public void givenValidEmptyArrayInsideArray_testStrictModeTrue_shouldNotThrowJsonException(){
79+
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
80+
.withStrictMode(true);
81+
82+
String testCase = "[[]]";
83+
84+
JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
85+
86+
assertEquals(testCase, jsonArray.toString());
87+
}
88+
89+
@Test
90+
public void givenValidEmptyArrayInsideArray_testStrictModeFalse_shouldNotThrowJsonException(){
91+
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
92+
.withStrictMode(false);
93+
94+
String testCase = "[[]]";
95+
96+
JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
97+
98+
assertEquals(testCase, jsonArray.toString());
99+
}
100+
66101
@Test
67102
public void givenInvalidString_testStrictModeTrue_shouldThrowJsonException() {
68103
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
@@ -72,7 +107,7 @@ public void givenInvalidString_testStrictModeTrue_shouldThrowJsonException() {
72107

73108
JSONException je = assertThrows(JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
74109

75-
assertEquals("Value is not surrounded by quotes: badString", je.getMessage());
110+
assertEquals("Value 'badString' is not surrounded by quotes at 10 [character 11 line 1]", je.getMessage());
76111
}
77112

78113
@Test
@@ -121,7 +156,7 @@ public void givenInvalidInputArray_testStrictModeTrue_shouldThrowInvalidCharacte
121156
JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
122157
JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
123158

124-
assertEquals("invalid character found after end of array: ; at 6 [character 7 line 1]", je.getMessage());
159+
assertEquals("invalid character ';' found after end of array at 6 [character 7 line 1]", je.getMessage());
125160
}
126161

127162
@Test
@@ -134,7 +169,7 @@ public void givenInvalidInputArrayWithNumericStrings_testStrictModeTrue_shouldTh
134169
JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
135170
JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
136171

137-
assertEquals("invalid character found after end of array: ; at 10 [character 11 line 1]", je.getMessage());
172+
assertEquals("invalid character ';' found after end of array at 10 [character 11 line 1]", je.getMessage());
138173
}
139174

140175
@Test
@@ -147,7 +182,7 @@ public void givenInvalidInputArray_testStrictModeTrue_shouldThrowValueNotSurroun
147182
JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
148183
JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
149184

150-
assertEquals("Value is not surrounded by quotes: implied", je.getMessage());
185+
assertEquals("Value 'implied' is not surrounded by quotes at 17 [character 18 line 1]", je.getMessage());
151186
}
152187

153188
@Test
@@ -206,7 +241,7 @@ public void givenUnbalancedQuotes_testStrictModeFalse_shouldThrowJsonException()
206241
JSONException jeTwo = assertThrows(JSONException.class,
207242
() -> new JSONArray(testCaseTwo, jsonParserConfiguration));
208243

209-
assertEquals("Expected a ',' or ']' at 10 [character 11 line 1]", jeOne.getMessage());
244+
assertEquals("Unterminated string. Character with int code 0 is not allowed within a quoted string. at 15 [character 16 line 1]", jeOne.getMessage());
210245
assertEquals("Unterminated string. Character with int code 0 is not allowed within a quoted string. at 15 [character 16 line 1]", jeTwo.getMessage());
211246
}
212247

@@ -220,7 +255,7 @@ public void givenInvalidInputArray_testStrictModeTrue_shouldThrowKeyNotSurrounde
220255
JSONException je = assertThrows("expected non-compliant array but got instead: " + testCase,
221256
JSONException.class, () -> new JSONArray(testCase, jsonParserConfiguration));
222257

223-
assertEquals(String.format("Value is not surrounded by quotes: %s", "test"), je.getMessage());
258+
assertEquals("Value 'test' is not surrounded by quotes at 6 [character 7 line 1]", je.getMessage());
224259
}
225260

226261
@Test
@@ -251,6 +286,9 @@ public void verifyMaxDepthThenDuplicateKey() {
251286
*/
252287
private List<String> getNonCompliantJSONList() {
253288
return Arrays.asList(
289+
"[1],",
290+
"[[1]\"sa\",[2]]a",
291+
"[1],\"dsa\": \"test\"",
254292
"[[a]]",
255293
"[]asdf",
256294
"[]]",

0 commit comments

Comments
 (0)