Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

884: Strict Mode for JSONObject #903

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
40 changes: 27 additions & 13 deletions src/main/java/org/json/JSONObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,13 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration
case 0:
throw x.syntaxError("A JSONObject text must end with '}'");
case '}':
if (jsonParserConfiguration.isStrictMode()) {
c = x.nextClean();
if (c != 0) {
throw x.syntaxError(String.format("Invalid char '%s' after '}'", c));
}
x.back();
}
return;
default:
key = x.nextSimpleValue(c, jsonParserConfiguration).toString();
Expand Down Expand Up @@ -254,20 +261,27 @@ public JSONObject(JSONTokener x, JSONParserConfiguration jsonParserConfiguration
// Pairs are separated by ','.

switch (x.nextClean()) {
case ';':
case ',':
if (x.nextClean() == '}') {
case ';':
case ',':
if (x.nextClean() == '}') {
return;
}
if (x.end()) {
throw x.syntaxError("A JSONObject text must end with '}'");
}
x.back();
break;
case '}':
if (jsonParserConfiguration.isStrictMode()) {
c = x.nextClean();
if (c != 0 && c != '}' && c != ',' && c != ']') {
throw x.syntaxError(String.format("Invalid char '%s' after '}'", c));
}
x.back();
}
return;
}
if (x.end()) {
throw x.syntaxError("A JSONObject text must end with '}'");
}
x.back();
break;
case '}':
return;
default:
throw x.syntaxError("Expected a ',' or '}'");
default:
throw x.syntaxError("Expected a ',' or '}'");
}
}
}
Expand Down
128 changes: 64 additions & 64 deletions src/main/java/org/json/JSONTokener.java
Original file line number Diff line number Diff line change
Expand Up @@ -334,63 +334,71 @@ public char nextClean() throws JSONException {
* @return A String.
* @throws JSONException Unterminated string or unbalanced quotes if strictMode == true.
*/
public String nextString(char quote) throws JSONException {
char c;
StringBuilder sb = new StringBuilder();
for (;;) {
c = this.next();
switch (c) {
case 0:
case '\n':
case '\r':
throw this.syntaxError("Unterminated string. " +
"Character with int code " + (int) c + " is not allowed within a quoted string.");
case '\\':
c = this.next();
switch (c) {
case 'b':
sb.append('\b');
break;
case 't':
sb.append('\t');
break;
case 'n':
sb.append('\n');
break;
case 'f':
sb.append('\f');
break;
case 'r':
sb.append('\r');
break;
case 'u':
String next = this.next(4);
try {
sb.append((char) Integer.parseInt(next, 16));
} catch (NumberFormatException e) {
throw this.syntaxError("Illegal escape. " +
"\\u must be followed by a 4 digit hexadecimal number. \\" + next
+ " is not valid.",
e);
}
break;
case '"':
case '\'':
case '\\':
case '/':
sb.append(c);
break;
default:
throw this.syntaxError("Illegal escape. Escape sequence \\" + c + " is not valid.");
}
break;
default:
if (c == quote) {
return sb.toString();
}
sb.append(c);
public Object nextString(char quote, boolean strictMode) throws JSONException {
if (strictMode && quote == '\'') {
throw this.syntaxError("Single quote wrap not allowed in strict mode");
}

if (quote == '"' || quote == '\'') {
char c;
StringBuilder sb = new StringBuilder();
for (; ; ) {
c = this.next();
switch (c) {
case 0:
case '\n':
case '\r':
throw this.syntaxError("Unterminated string. " +
"Character with int code " + (int) c + " is not allowed within a quoted string.");
case '\\':
c = this.next();
switch (c) {
case 'b':
sb.append('\b');
break;
case 't':
sb.append('\t');
break;
case 'n':
sb.append('\n');
break;
case 'f':
sb.append('\f');
break;
case 'r':
sb.append('\r');
break;
case 'u':
String next = this.next(4);
try {
sb.append((char) Integer.parseInt(next, 16));
} catch (NumberFormatException e) {
throw this.syntaxError("Illegal escape. " +
"\\u must be followed by a 4 digit hexadecimal number. \\" + next
+ " is not valid.",
e);
}
break;
case '"':
case '\'':
case '\\':
case '/':
sb.append(c);
break;
default:
throw this.syntaxError("Illegal escape. Escape sequence \\" + c + " is not valid.");
}
break;
default:
if (c == quote) {
return sb.toString();
}
sb.append(c);
}
}
}

return parsedUnquotedText(quote, strictMode);
}

/**
Expand Down Expand Up @@ -528,15 +536,7 @@ private JSONArray getJsonArray() {
Object nextSimpleValue(char c, JSONParserConfiguration jsonParserConfiguration) {
boolean strictMode = jsonParserConfiguration.isStrictMode();

if (strictMode && c == '\'') {
throw this.syntaxError("Single quote wrap not allowed in strict mode");
}

if (c == '"' || c == '\'') {
return this.nextString(c);
}

return parsedUnquotedText(c, strictMode);
return this.nextString(c, strictMode);
}

/**
Expand Down
60 changes: 48 additions & 12 deletions src/test/java/org/json/junit/JSONParserConfigurationTest.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package org.json.junit;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
Expand Down Expand Up @@ -46,6 +42,48 @@ public void givenInvalidInputArrays_testStrictModeTrue_shouldThrowJsonException(
() -> new JSONArray(testCase, jsonParserConfiguration)));
}

@Test
public void givenInvalidJsonObjects_testStrictModeTrue_shouldThrowJsonException() {
final List<String> jsonObjects = Arrays.asList(
"{}}",
"{}abc",
"{{}"
);

jsonObjects.forEach(jsonObject ->
assertThrows(JSONException.class,
() -> new JSONObject(jsonObject, new JSONParserConfiguration().withStrictMode(true))));
}

@Test
public void givenValidArraysWithInvalidJsonObjects_testStrictMode_shouldThrowJsonException() {
final List<String> jsonArrays = Arrays.asList(
"[1, {}}]",
"[1, 2, 3, \"value\", true]abc",
"[{}, {\"key\":\"value\"}abc]"
);

jsonArrays.forEach(jsonArray ->
assertThrows(JSONException.class,
() -> new JSONArray(jsonArray, new JSONParserConfiguration().withStrictMode(true))));
}

@Test
public void givenValidJsonObjects_testStrictModeTrue() {
final List<String> jsonPaths = Arrays.asList(
"src/test/resources/Issue884-validJsonObj.json"
);

jsonPaths.forEach(path -> {
final String resultJsonAsString = Util.getJsonStringFromFilePath(Paths.get(path));

final JSONObject resultJsonObject = new JSONObject(resultJsonAsString,
new JSONParserConfiguration().withStrictMode(true));

assertEquals(resultJsonAsString.replaceAll("\\s", "").length(), resultJsonObject.toString().length());
});
}

@Test
public void givenEmptyArray_testStrictModeTrue_shouldNotThrowJsonException() {
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
Expand Down Expand Up @@ -126,15 +164,13 @@ public void shouldHandleNumericArray() {
}

@Test
public void givenCompliantJSONArrayFile_testStrictModeTrue_shouldNotThrowAnyException() throws IOException {
try (Stream<String> lines = Files.lines(Paths.get("src/test/resources/compliantJsonArray.json"))) {
String compliantJsonArrayAsString = lines.collect(Collectors.joining());
JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
.withStrictMode(true);

new JSONArray(compliantJsonArrayAsString, jsonParserConfiguration);
}
public void givenCompliantJSONArrayFile_testStrictModeTrue_shouldNotThrowAnyException() {
final String compliantJsonArrayAsString = Util.getJsonStringFromFilePath(
Paths.get("src/test/resources/compliantJsonArray.json"));
final JSONParserConfiguration jsonParserConfiguration = new JSONParserConfiguration()
.withStrictMode(true);

new JSONArray(compliantJsonArrayAsString, jsonParserConfiguration);
}

@Test
Expand Down
18 changes: 16 additions & 2 deletions src/test/java/org/json/junit/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@

import static org.junit.Assert.*;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.json.*;

/**
Expand All @@ -17,6 +22,14 @@
*/
public class Util {

public static String getJsonStringFromFilePath(final Path path) {
try (final Stream<String> lines = Files.lines(path)) {
return lines.collect(Collectors.joining());
} catch (final IOException ioe) {
throw new IllegalStateException(String.format("Unable to retrieve path: %s", path));
}
}

/**
* Compares two JSONArrays for equality.
* The arrays need not be in the same order.
Expand All @@ -36,7 +49,7 @@ public static void compareActualVsExpectedJsonArrays(JSONArray jsonArray,

/**
* Compares two JSONObjects for equality. The objects need not be
* in the same order
* in the same order
* @param jsonObject created by the code to be tested
* @param expectedJsonObject created specifically for comparing
*/
Expand Down Expand Up @@ -141,14 +154,15 @@ public static void checkJSONObjectMaps(JSONObject jsonObject, Class<? extends Ma
assertTrue(mapType == ((JSONObject) val).getMapType());
checkJSONObjectMaps(jsonObjectVal, mapType);
} else if (val instanceof JSONArray) {
JSONArray jsonArrayVal = (JSONArray)val;
JSONArray jsonArrayVal = (JSONArray) val;
checkJSONArrayMaps(jsonArrayVal, mapType);
}
}
}

/**
* Asserts that all JSONObject maps in the JSONArray object match the default map
*
* @param jsonArrays list of JSONArray objects to be tested
*/
public static void checkJSONArraysMaps(List<JSONArray> jsonArrays) {
Expand Down
14 changes: 14 additions & 0 deletions src/test/resources/Issue884-validJsonObj.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"test": {
"key": "value",
"key2": "value",
"key3": {
"anotherNested": "obj",
"anArray": [
"val1",
1,
3
]
}
}
}
Loading