Skip to content

Commit

Permalink
feat(#877): improved JSONArray and JSONTokener logic
Browse files Browse the repository at this point in the history
JSONArray construction improved to recursive validation
JSONTokener implemented smallCharMemory and array level for improved validation
Added new test cases and minor test case adaption
  • Loading branch information
rikkarth committed Apr 27, 2024
1 parent 879579d commit 9216a19
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 79 deletions.
124 changes: 56 additions & 68 deletions src/main/java/org/json/JSONArray.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,87 +96,75 @@ public JSONArray(JSONTokener x) throws JSONException {
*/
public JSONArray(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) throws JSONException {
this();
if (x.nextClean() != '[') {
char nextChar = x.nextClean();

// check first character, if not '[' throw JSONException
if (nextChar != '[') {
throw x.syntaxError("A JSONArray text must start with '['");
}

char nextChar = x.nextClean();
if (nextChar == 0) {
// array is unclosed. No ']' found, instead EOF
throw x.syntaxError("Expected a ',' or ']'");
}
if (nextChar != ']') {
x.back();
for (;;) {
if (x.nextClean() == ',') {
x.back();
this.myArrayList.add(JSONObject.NULL);
} else {
parseTokener(x, jsonParserConfiguration); // runs recursively

}

private void parseTokener(JSONTokener x, JSONParserConfiguration jsonParserConfiguration) {
boolean strictMode = jsonParserConfiguration.isStrictMode();

char cursor = x.nextClean();

switch (cursor) {
case 0:
throwErrorIfEoF(x);
break;
case ',':
cursor = x.nextClean();

throwErrorIfEoF(x);

if (cursor == ']') {
break;
}

x.back();

parseTokener(x, jsonParserConfiguration);
break;
case ']':
if (strictMode) {
cursor = x.nextClean();
boolean isNotEoF = !x.end();

if (isNotEoF && x.getArrayLevel() == 0) {
throw x.syntaxError(String.format("invalid character '%s' found after end of array", cursor));
}

x.back();
this.myArrayList.add(x.nextValue(jsonParserConfiguration));
}
switch (x.nextClean()) {
case 0:
// array is unclosed. No ']' found, instead EOF
throw x.syntaxError("Expected a ',' or ']'");
case ',':
nextChar = x.nextClean();
if (nextChar == 0) {
// array is unclosed. No ']' found, instead EOF
throw x.syntaxError("Expected a ',' or ']'");
}
if (nextChar == ']') {
return;
}
x.back();
break;
case ']':
if (jsonParserConfiguration.isStrictMode()) {
nextChar = x.nextClean();

if (nextChar == ','){
x.back();
return;
}

if (nextChar == ']'){
x.back();
return;
}

if (nextChar != 0) {
throw x.syntaxError("invalid character found after end of array: " + nextChar);
}
}

return;
default:
throw x.syntaxError("Expected a ',' or ']'");
break;
default:
x.back();
boolean currentCharIsQuote = x.getPrevious() == '"';
boolean quoteIsNotNextToValidChar = x.getPreviousChar() != ',' && x.getPreviousChar() != '[';

if (strictMode && currentCharIsQuote && quoteIsNotNextToValidChar) {
throw x.syntaxError(String.format("invalid character '%s' found after end of array", cursor));
}
}
}

if (jsonParserConfiguration.isStrictMode()) {
validateInput(x);
this.myArrayList.add(x.nextValue(jsonParserConfiguration));
parseTokener(x, jsonParserConfiguration);
}
}

/**
* Checks if Array adheres to strict mode guidelines, if not, throws JSONException providing back the input in the
* error message.
* Throws JSONException if JSONTokener has reached end of file, usually when array is unclosed. No ']' found,
* instead EoF.
*
* @param x tokener used to examine input.
* @throws JSONException if input is not compliant with strict mode guidelines;
* @param x the JSONTokener being evaluated.
* @throws JSONException if JSONTokener has reached end of file.
*/
private void validateInput(JSONTokener x) {
char cursor = x.getPrevious();

boolean isEndOfArray = cursor == ']';
char nextChar = x.nextClean();
boolean nextCharacterIsNotEoF = nextChar != 0;

if (isEndOfArray && nextCharacterIsNotEoF) {
throw x.syntaxError(String.format("Provided Array is not compliant with strict mode guidelines: '%s'", nextChar));
private void throwErrorIfEoF(JSONTokener x) {
if (x.end()) {
throw x.syntaxError(String.format("Expected a ',' or ']' but instead found '%s'", x.getPrevious()));
}
}

Expand Down
50 changes: 48 additions & 2 deletions src/main/java/org/json/JSONTokener.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

/*
Public Domain.
Expand Down Expand Up @@ -31,6 +33,8 @@ public class JSONTokener {
private boolean usePrevious;
/** the number of characters read in the previous line. */
private long characterPreviousLine;
private final List<Character> smallCharMemory;
private int arrayLevel = 0;


/**
Expand All @@ -49,6 +53,7 @@ public JSONTokener(Reader reader) {
this.character = 1;
this.characterPreviousLine = 0;
this.line = 1;
this.smallCharMemory = new ArrayList<>(2);
}


Expand Down Expand Up @@ -186,6 +191,46 @@ public char next() throws JSONException {
return this.previous;
}

private void insertCharacterInCharMemory(Character c) {
boolean foundSameCharRef = checkForEqualCharRefInMicroCharMemory(c);
if(foundSameCharRef){
return;
}

if(smallCharMemory.size() < 2){
smallCharMemory.add(c);
return;
}

smallCharMemory.set(0, smallCharMemory.get(1));
smallCharMemory.remove(1);
smallCharMemory.add(c);
}

private boolean checkForEqualCharRefInMicroCharMemory(Character c) {
boolean isNotEmpty = !smallCharMemory.isEmpty();
if (isNotEmpty) {
Character lastChar = smallCharMemory.get(smallCharMemory.size() - 1);
return c.compareTo(lastChar) == 0;
}

// list is empty so there's no equal characters
return false;
}

/**
* Retrieves the previous char from memory.
*
* @return previous char stored in memory.
*/
public char getPreviousChar() {
return smallCharMemory.get(0);
}

public int getArrayLevel(){
return this.arrayLevel;
}

/**
* Get the last character read from the input or '\0' if nothing has been read yet.
* @return the last character read from the input.
Expand Down Expand Up @@ -263,7 +308,6 @@ public String next(int n) throws JSONException {
return new String(chars);
}


/**
* Get the next char in the string, skipping whitespace.
* @throws JSONException Thrown if there is an error reading the source string.
Expand All @@ -273,6 +317,7 @@ public char nextClean() throws JSONException {
for (;;) {
char c = this.next();
if (c == 0 || c > ' ') {
insertCharacterInCharMemory(c);
return c;
}
}
Expand Down Expand Up @@ -441,6 +486,7 @@ public Object nextValue(JSONParserConfiguration jsonParserConfiguration) throws
case '[':
this.back();
try {
this.arrayLevel++;
return new JSONArray(this, jsonParserConfiguration);
} catch (StackOverflowError e) {
throw new JSONException("JSON Array or Object depth too large to process.", e);
Expand Down Expand Up @@ -531,7 +577,7 @@ private Object getValidNumberBooleanOrNullFromObject(Object value) {
return value;
}

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

/**
Expand Down
6 changes: 3 additions & 3 deletions src/test/java/org/json/junit/JSONArrayTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public void unclosedArray() {
assertNull("Should throw an exception", new JSONArray("["));
} catch (JSONException e) {
assertEquals("Expected an exception message",
"Expected a ',' or ']' at 1 [character 2 line 1]",
"Expected a ',' or ']' but instead found '[' at 1 [character 2 line 1]",
e.getMessage());
}
}
Expand All @@ -157,7 +157,7 @@ public void unclosedArray2() {
assertNull("Should throw an exception", new JSONArray("[\"test\""));
} catch (JSONException e) {
assertEquals("Expected an exception message",
"Expected a ',' or ']' at 7 [character 8 line 1]",
"Expected a ',' or ']' but instead found '\"' at 7 [character 8 line 1]",
e.getMessage());
}
}
Expand All @@ -172,7 +172,7 @@ public void unclosedArray3() {
assertNull("Should throw an exception", new JSONArray("[\"test\","));
} catch (JSONException e) {
assertEquals("Expected an exception message",
"Expected a ',' or ']' at 8 [character 9 line 1]",
"Expected a ',' or ']' but instead found ',' at 8 [character 9 line 1]",
e.getMessage());
}
}
Expand Down
50 changes: 44 additions & 6 deletions src/test/java/org/json/junit/JSONParserConfigurationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ public void givenInvalidInputArrays_testStrictModeTrue_shouldThrowJsonException(
() -> new JSONArray(testCase, jsonParserConfiguration)));
}

@Test
public void givenEmptyArray_testStrictModeTrue_shouldNotThrowJsonException(){
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
.withStrictMode(true);

String testCase = "[]";

JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);
System.out.println(jsonArray);
}

@Test
public void givenValidDoubleArray_testStrictModeTrue_shouldNotThrowJsonException() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
Expand All @@ -63,6 +74,30 @@ public void givenValidDoubleArray_testStrictModeTrue_shouldNotThrowJsonException
assertTrue(arrayShouldContainBooleanAt0.get(0) instanceof Boolean);
}

@Test
public void givenValidEmptyArrayInsideArray_testStrictModeTrue_shouldNotThrowJsonException(){
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
.withStrictMode(true);

String testCase = "[[]]";

JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);

assertEquals(testCase, jsonArray.toString());
}

@Test
public void givenValidEmptyArrayInsideArray_testStrictModeFalse_shouldNotThrowJsonException(){
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
.withStrictMode(false);

String testCase = "[[]]";

JSONArray jsonArray = new JSONArray(testCase, jsonParserConfiguration);

assertEquals(testCase, jsonArray.toString());
}

@Test
public void givenInvalidString_testStrictModeTrue_shouldThrowJsonException() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
Expand All @@ -72,7 +107,7 @@ public void givenInvalidString_testStrictModeTrue_shouldThrowJsonException() {

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

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

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

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

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

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

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

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

@Test
Expand Down Expand Up @@ -206,7 +241,7 @@ public void givenUnbalancedQuotes_testStrictModeFalse_shouldThrowJsonException()
JSONException jeTwo = assertThrows(JSONException.class,
() -> new JSONArray(testCaseTwo, jsonParserConfiguration));

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

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

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

@Test
Expand Down Expand Up @@ -251,6 +286,9 @@ public void verifyMaxDepthThenDuplicateKey() {
*/
private List<String> getNonCompliantJSONList() {
return Arrays.asList(
"[1],",
"[[1]\"sa\",[2]]a",
"[1],\"dsa\": \"test\"",
"[[a]]",
"[]asdf",
"[]]",
Expand Down

0 comments on commit 9216a19

Please sign in to comment.