feat(#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
This commit is contained in:
rikkarth
2024-04-27 22:14:35 +01:00
parent 879579d3bb
commit 9216a19366
4 changed files with 148 additions and 76 deletions

View File

@@ -96,87 +96,75 @@ public class JSONArray implements Iterable<Object> {
*/
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 {
x.back();
this.myArrayList.add(x.nextValue(jsonParserConfiguration));
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;
}
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;
}
x.back();
if (nextChar == ']'){
x.back();
return;
}
parseTokener(x, jsonParserConfiguration);
break;
case ']':
if (strictMode) {
cursor = x.nextClean();
boolean isNotEoF = !x.end();
if (nextChar != 0) {
throw x.syntaxError("invalid character found after end of array: " + nextChar);
}
}
if (isNotEoF && x.getArrayLevel() == 0) {
throw x.syntaxError(String.format("invalid character '%s' found after end of array", cursor));
}
return;
default:
throw x.syntaxError("Expected a ',' or ']'");
x.back();
}
}
}
break;
default:
x.back();
boolean currentCharIsQuote = x.getPrevious() == '"';
boolean quoteIsNotNextToValidChar = x.getPreviousChar() != ',' && x.getPreviousChar() != '[';
if (jsonParserConfiguration.isStrictMode()) {
validateInput(x);
if (strictMode && currentCharIsQuote && quoteIsNotNextToValidChar) {
throw x.syntaxError(String.format("invalid character '%s' found after end of array", cursor));
}
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()));
}
}

View File

@@ -2,6 +2,8 @@ package org.json;
import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/*
Public Domain.
@@ -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;
/**
@@ -49,6 +53,7 @@ public class JSONTokener {
this.character = 1;
this.characterPreviousLine = 0;
this.line = 1;
this.smallCharMemory = new ArrayList<>(2);
}
@@ -186,6 +191,46 @@ public class JSONTokener {
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.
@@ -263,7 +308,6 @@ public class JSONTokener {
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.
@@ -273,6 +317,7 @@ public class JSONTokener {
for (;;) {
char c = this.next();
if (c == 0 || c > ' ') {
insertCharacterInCharMemory(c);
return c;
}
}
@@ -441,6 +486,7 @@ public class JSONTokener {
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);
@@ -531,7 +577,7 @@ public class JSONTokener {
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));
}
/**